diff --git a/website/docs/contributing/ADRs/ADRs.md b/website/docs/contributing/ADRs/ADRs.md index 5acfa6618e6..d213083a374 100644 --- a/website/docs/contributing/ADRs/ADRs.md +++ b/website/docs/contributing/ADRs/ADRs.md @@ -25,6 +25,7 @@ We are in the process of defining ADRs for the back end. At the time of writing * [Breaking DB changes](./back-end/breaking-db-changes.md) * [POST/PUT API payload](./back-end/POST-PUT-api-payload.md) * [Specificity in database column references](./back-end/specificity-db-columns.md) +* [Write model vs Read models](./back-end/write-model-vs-read-models.md) ## Front-end ADRs diff --git a/website/docs/contributing/ADRs/back-end/write-model-vs-read-models.md b/website/docs/contributing/ADRs/back-end/write-model-vs-read-models.md new file mode 100644 index 00000000000..c1e6233533b --- /dev/null +++ b/website/docs/contributing/ADRs/back-end/write-model-vs-read-models.md @@ -0,0 +1,80 @@ +--- +title: "ADR: Write Model vs Read Models" +--- + +## Background + +We keep solving 3 problems in each of the business modules: +* changing the state of the system through actions +* displaying UI +* asking other modules for information + +In the past we had one pattern of `service + store` for handling all 3 of those. Store was responsible for changing state and returning UI data. Cross module collaboration +happened by calling services in other modules. While it was convenient and reduced the number of building blocks to learn, it also led to coupling between +services and overloaded stores - responsible for writing data, reading complex UI data and answering questions from other modules. + +## Decision + +To address these challenges categorize your problem as one of 3 models: + +[3 types of models](/img/write-model-vs-read-models.png) + +We have 3 types of models serving 3 different purposes: +* I need to take action that modifies a state of the system (Write Model) + +Solution: Go through the Command stack aka the *Write Model*. *Application logic* with the use case/workflow/steps resides in the *Application Service*. Application service can delegate + business logic handling to the *Domain Model* if needed (only for complex subdomains like Change Requests). Simple domain logic (very few business if statements) + can be handled together with the application logic. Application service calls the store that is responsible for the *generic CRUD operations* serving the write model. + +* I need to read data for UI display (External Read Model) + +Solution: Expose *External Read Model* aka *View Model* that returns all the data in one query so that frontend doesn't have to ask multiple write models for data. + View model will typically join data across a few DB tables since most UI screens required more than one table of data. If we keep the *complex queries* for UI + in the external read model our stores can be generic and focused on the main table operations. + +* I need information from another module to proceed with my module application logic (Internal Read Model) + +Solution: Cross module queries can be handled with the *Internal Read Model*. Internal means it's used internally between our modules and not exposed to the UI. + Internal read models are typically narrowly focused on answering one question and usually require *simple queries* compared to external read models. By introducing internal + read model other business modules are not coupled to our module's service/store/write model and can't call write model methods from another module by accident. + +## Example + +Before (one multi-purpose class) +```typescript +class SegmentStore { + // used to perform actions on segment + create(segment: Segment): Promise {} + get(id: number): Promise {} + update(id: number, segment: Segment): Promise {} + delete(id: number): Promise {} + + // used by UI + getAll(): Promise {} + + // used by another module checking existing names + getSegmentNames(): Promise {} +} +``` + +After (3 role-based classes so clients depends only on method they use) +```typescript +// used to perform actions on segment +class SegmentStore { + create(segment: Segment): Promise {} + get(id: number): Promise {} + update(id: number, segment: Segment): Promise {} + delete(id: number): Promise {} +} + +// used by UI +class SegmentViewModel { + getAll(): Promise {} +} + +// used by another module checking existing names +class SegmentReadModel { + getSegmentNames(): Promise {} +} +``` + diff --git a/website/static/img/write-model-vs-read-models.png b/website/static/img/write-model-vs-read-models.png new file mode 100644 index 00000000000..25a4137151f Binary files /dev/null and b/website/static/img/write-model-vs-read-models.png differ