-
Notifications
You must be signed in to change notification settings - Fork 2
Project Architecture
As this project grows, a lot of the common issues have started to pop up and it's time to refactor to an established architecture
- The project currently is extremely tightly coupled between the UI and the logic
- Because there's so much coupling, any time there's changes needed to the logic for whatever reason, there's a lot of jumping around in the codebase we have to do to fix it (we have run into this issue several times now)
- The project, as currently setup, is impossible to port this over to a React Native app
- Testing is really difficult, if not impossible
The solution that has been decided to be used is definitely not the only one, but it is the one that has been chosen. We will be using a variation of hexagonal architecture. I say variation because it would be impossible to replicate it 1 for 1 in this project as it is typically used in backend systems.
This architecture will solve all of the above problems:
- Decouple the ui from the logic
- Any changes in dependencies or logic won't affect the UI and won't require drastic changes (it will be much simpler to do)
- Makes testing much easier
- Can easily connect a React Native app and make use of the same logic
If you looked at examples of hexagonal architecture on google, you'd probably see a flow something like input from a rest endpoint that gets sent to a usecase where business logic is contained. This then goes to some output, like a Postgres adapter. In this project, think of it as a division between the client and the server. The client is the input, and the server is the output. We are also splitting it up into modules and not just one output for the entire server.
Here is a sloppy diagram I drew up
Everything on the primary side will be related to the client implementations (web, mobile, desktop, etc). Everything on the secondary side will be related to the server. In this project I think there will only be just the rest api we have. I don't see us using anything else.
Here is the general flow (using login as an example):
- When user clicks on sign in button, this will trigger the submit handler
- The submit handler calls the auth adapter (primary)
- The auth adapter goes to the login usecase.
- The usecase is where any business logic gets executed if there is any. If not, or after the logic is finished processing, it then goes to the adapter (secondary)
- The secondary adapter is where the call to the login endpoint happens
The port just sets up a contract on what should be accessible from the outside of the application layer. The adapter has the implementation details of the port.
In this project, the modules folder is the "hexagon". All the logic that is not strictly tied to the client should be moved inside here. Do not make any direct calls to a usecase. Everything should flow through the adapter.
An example of something that would be client specific is Tanstack Query. An example of something that wouldn't be client specific is data transformations. An example of something that is not client specific is handling data fetched from the backend.
TODO:
I'm not entirely sure how synchronous logic should be handled yet (data transformations, status checks, validation). Will update this when that's figured out