Skip to content

Commit

Permalink
docs: ADR for 3 types of models (#6436)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Mar 12, 2024
1 parent 9c29461 commit 74df643
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 0 deletions.
1 change: 1 addition & 0 deletions website/docs/contributing/ADRs/ADRs.md
Expand Up @@ -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

Expand Down
@@ -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<Segment> {}
get(id: number): Promise<Segment> {}
update(id: number, segment: Segment): Promise<Segment> {}
delete(id: number): Promise<void> {}

// used by UI
getAll(): Promise<SegmentWithUsageInfo[]> {}

// used by another module checking existing names
getSegmentNames(): Promise<string[]> {}
}
```

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<Segment> {}
get(id: number): Promise<Segment> {}
update(id: number, segment: Segment): Promise<Segment> {}
delete(id: number): Promise<void> {}
}

// used by UI
class SegmentViewModel {
getAll(): Promise<SegmentWithUsageInfo[]> {}
}

// used by another module checking existing names
class SegmentReadModel {
getSegmentNames(): Promise<string[]> {}
}
```

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 74df643

Please sign in to comment.