|
| 1 | +# petmemo |
| 2 | + |
| 3 | +This is a document describing the architecture, known limitations and shortcomings of the [petmemo](https://github.com/sarneeh/petmemo) game. You will learn about how the application has been structured and what was the idea and rationale behind it. |
| 4 | + |
| 5 | +# Architecture |
| 6 | + |
| 7 | +## Inspiration |
| 8 | + |
| 9 | +The architecture is inspired by a few resources found in the web: |
| 10 | + |
| 11 | +- [Feature-Sliced Design](https://feature-sliced.design/) |
| 12 | +- [Khalil Stemmler - Client-Side Architecture](https://khalilstemmler.com/articles/client-side-architecture/introduction/) |
| 13 | +- [Juan Otálora - Folder structure in a React hexagonal architecture](https://dev.to/juanoa/folder-structure-in-a-react-hexagonal-architecture-h77) |
| 14 | + |
| 15 | +It does not strictly follow any of those suggestions, but it's inspired by some ideas from each of them to compose a solution that suits the needs of the project. |
| 16 | + |
| 17 | +## Structure |
| 18 | + |
| 19 | +The app is devided in 4 base layers: **app, modules, pages** and **shared**. Some of the layers have sub-layers (modules, shared) and some are flat (app, pages). |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +You can find detailed information about each of the layers (and its sub-layers) below. |
| 24 | + |
| 25 | +### App |
| 26 | + |
| 27 | +Simple, flat layer where we put all app-wide settings, styles, providers, routing etc. Elements of this layers should not be related to any domain logic of the application and suit mostly for app bootstrap. |
| 28 | + |
| 29 | +### Modules |
| 30 | + |
| 31 | +The core of the whole application. It consists of domain slices that are separated in sub-layers, similar to the [shared](#shared) layer. Elements from this layer are used to build up whole pages in the [pages](#pages) layer. |
| 32 | + |
| 33 | +#### Model |
| 34 | + |
| 35 | +This sub-layer consists of multiple elements related to the domain slice: |
| 36 | + |
| 37 | +- `state` - state management store, consists of module **state** (data) and **actions** (app events) |
| 38 | +- `selectors` - reusable state selectors |
| 39 | +- `hooks` - React hooks that encapsulate some of the app state selection and actions (domain logic) for increased testability |
| 40 | +- `subscriptions` - state subscription creators to simplify implementation of state change side-effects and increase reusability |
| 41 | +- `types` - TypeScript types and interfaces related to the domain slice |
| 42 | + |
| 43 | +Those are building blocks that encapsulate some of the domain logic to decrease inlined domain logic in the module [ui](#ui) components. |
| 44 | + |
| 45 | +#### Infrastructure |
| 46 | + |
| 47 | +Infrastructure elements related to the domain. Often composed from reusable elements from `shared/infrastructure`. |
| 48 | + |
| 49 | +Examples: `CardRepository`, `CardAPIClient` |
| 50 | + |
| 51 | +#### Services |
| 52 | + |
| 53 | +Classes that help decouple some of the domain logic. Ideally split to as small chunks as possible to allow easier testability and reusability. |
| 54 | + |
| 55 | +#### UI |
| 56 | + |
| 57 | +Domain-related components, often "smart" with logic in it. |
| 58 | + |
| 59 | +Examples: `GameBoardCard`, `GameStartButton` |
| 60 | + |
| 61 | +#### Utils |
| 62 | + |
| 63 | +Helper functions related to the domain that do not fit directly to any service or that may be used by multiple services. |
| 64 | + |
| 65 | +Examples: `isCardRevealed` |
| 66 | + |
| 67 | +### Pages |
| 68 | + |
| 69 | +Compositional layer that consists of components that construct full pages from various app modules. Should not contain any domain logic. |
| 70 | + |
| 71 | +### Shared |
| 72 | + |
| 73 | +Shared layer that consists of elements that are not related to any domain. |
| 74 | + |
| 75 | +#### Infrastructure |
| 76 | + |
| 77 | +Reusable infrastructure elements like API clients, state managament addons. |
| 78 | + |
| 79 | +Examples: pre-configured API client with error handling, interceptors, state management offline storage hooks |
| 80 | + |
| 81 | +#### UI |
| 82 | + |
| 83 | +Reusable pure components not related to any domain logic. |
| 84 | + |
| 85 | +Examples: `Button`, `Card` |
| 86 | + |
| 87 | +#### Utils |
| 88 | + |
| 89 | +Reusable helper functions not related to any domain logic. |
| 90 | + |
| 91 | +Examples: `replaceItemAtIndex`, `preloadImages` |
| 92 | + |
| 93 | +## Rules |
| 94 | + |
| 95 | +There are a few rules that need to be followed so that the architecture would make sense and so it wouldn't provide more problems than benefits: |
| 96 | + |
| 97 | +- All of the `modules` should have clear entrypoints and only those entrypoints should be used. This ensures encapsulation and usage only of the publicly exposed interfaces. Example: |
| 98 | + - bad: `import { GameCard } from '@/modules/game/ui/GameCard'` |
| 99 | + - good: `import { GameCard } from '@/modules/game'` |
| 100 | +- There should be clear importing rules throughout the different layers to prevent unwanted situations like: |
| 101 | + - `app` importing from the `modules` layer |
| 102 | + - `pages` importing from `app` layer |
| 103 | + - `modules` importing from `pages` layer |
| 104 | + |
| 105 | +### Automation |
| 106 | + |
| 107 | +Humans are bad in remembering things and ensuring everything is in the right order. When there are tight deadlines - everything will eventually fall apart 🙂 To prevent this, we should automate the process as much as possible to ensure architecture consistency. |
| 108 | + |
| 109 | +For this, petmemo is using a great eslint plugin called [eslint-plugin-boundaries](https://github.com/javierbrea/eslint-plugin-boundaries), which [properly configured](https://github.com/sarneeh/petmemo/blob/main/.eslintrc.cjs#L81-L116) can automate checking the rules described above. |
| 110 | + |
| 111 | +## Rationale |
| 112 | + |
| 113 | +This architecture is something I've been working on on various project and it's still an in-progress. It's definitely scaling a lot better than the most common flat components/containers approach and results in a lot better code structure thanks to the separation of domain elements. |
| 114 | + |
| 115 | +The biggest benefits that I've found comparing to other architectures is: |
| 116 | + |
| 117 | +- clear distinction between UI and general logic that's tied to some kind of a domain or if it's for general purpose |
| 118 | +- better testability and maintainability of the domain logic |
| 119 | + - thanks to splitting the domain into reusable hooks/services/selectors you can test small chunks of logic with a small (or none at all) amount of mocking |
| 120 | +- easily extendable |
| 121 | +- can be easily split into multiple packages |
| 122 | + - thanks to the `modules/*` approach with clear entrypoints (public interfaces) you can easily split this code to a separate npm package that could be reused in an another application |
| 123 | + - `shared/*` could also be split to a separate package |
| 124 | + |
| 125 | +It's definitely not perfect and it might not be as scalable for a lot bigger projects (didn't had the opportunity to test it out in this kind of scenario yet), but it works fine in small-mid sizes ones for sure. |
| 126 | + |
| 127 | +For bigger sized projects I'd definitely try out to integrate [https://nx.dev/](https://nx.dev/). |
0 commit comments