Slide 1

Slide 1 text

DDD on the frontend @talyssonoc | @codeminer42 Theory and practice

Slide 2

Slide 2 text

Talysson Oliveira Software architect & Technical development manager at Codeminer42 @talyssonoc beacons.ai/talyssonoc

Slide 3

Slide 3 text

codeminer42.com @codeminer42

Slide 4

Slide 4 text

Slide 5

Slide 5 text

Example scenario: Project management tool

Slide 6

Slide 6 text

Project management tool - The app supports multiple Teams - A Team is a group of people that work together toward common goals - Each Team can have multiple Projects - A Project is a continuous or finite plan to achieve a goal - A user of the app can be Member of multiple Teams - Inside a Team, a Member can be a Participant of multiple Projects - A Project contains multiple Tasks: - Type: Features, Chores and Bugfixes - Status: Unplanned, Unstarted, Started, Finished, Accepted - …

Slide 7

Slide 7 text

Project management tool context map Shared kernel Customer/Supplier Tasks Teams & Projects D Identity & Access U

Slide 8

Slide 8 text

Discovering the frontend domain model

Slide 9

Slide 9 text

Check the mockups 💡

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Talk to domain experts and the UX team

Slide 12

Slide 12 text

Board Columns Sprints Project velocity (can be simulated) Estimate A draft of a task ✏ … Not estimated

Slide 13

Slide 13 text

Board Velocity Simulated velocity Backlog sprints Current sprint Task draft Columns Change tasks positions Task position Priority

Slide 14

Slide 14 text

Trying to make some sense of the business rules - A Participant interacts with the Tasks through the Board - The Columns are divided according to the Status of the Tasks - The Backlog has Tasks that are Planned but not Accepted yet - The Project Velocity is calculated by the backend somehow - The Project Velocity is used to separate the Backlog into Sprints - It is possible to use a Simulated Velocity - …

Slide 15

Slide 15 text

Characteristics of the frontend - Not just visual rules and behaviors - Visual representations that implement and enforce business rules - It has its own particular concepts and business rules - It has its own Ubiquitous Language - It has its own domain model

Slide 16

Slide 16 text

The frontend belong to its own Bounded Context!

Slide 17

Slide 17 text

Project management tool context map Shared kernel Customer/Supplier Frontend Tasks Teams & Projects D Identity & Access U

Slide 18

Slide 18 text

Project management tool context map Shared kernel Customer/Supplier Board (Frontend) Tasks Teams & Projects D Identity & Access U

Slide 19

Slide 19 text

Project management tool context map Shared kernel Customer/Supplier Customer/Supplier Board (Frontend) D D Tasks Teams & Projects D U Identity & Access U U Partnership Customer/Supplier

Slide 20

Slide 20 text

Challenges

Slide 21

Slide 21 text

Ensuring the model integrity

Slide 22

Slide 22 text

Ensuring model integrity - The browser can't be relied on as the source of truth - Domain rules shared by frontend and server, when performed on the frontend, can't be automatically trusted by the server - Domain logic on the frontend is also a matter of UX - We can’t ignore those technical limitations - How do we ensure the integrity of the model then?

Slide 23

Slide 23 text

Example use case: Participant creates a new Task

Slide 24

Slide 24 text

Composition of the Task aggregate - ID: unique - Title: unique inside its Project - Description: optional - Type: Feature, Chore or Bugfix - Status: Unplanned, Unstarted, Started, Finished or Accepted - Requester and Assignee: - Both are Participants of the Project - Unplanned and Unstarted Tasks might not have an Assignee - Estimate: 1, 2, 3, 5 or 8, only Planned features can have an Estimate - Priority: the lower the value, the highest is the priority

Slide 25

Slide 25 text

"Participant creates a new Task" business rules - A Task should never be created in the Accepted status - The Participant who created it is the Requester - A Task is always created with the lowest priority

Slide 26

Slide 26 text

Form to create a new Task { A Task

Slide 27

Slide 27 text

Form to create a new Task { A Task (right?) Unique? ? ? Priority ID

Slide 28

Slide 28 text

Form to create a new Task { A Task (right?) Unique? ? ? Priority ID

Slide 29

Slide 29 text

Aggregate design on the frontend - Some invariants conform to models implemented on a server - Changes on those aggregates should not be trusted until validated - Special case: generating a new id on the browser - We can't trust that the generation algorithm is legit - Even if we use something like randomUUID(), it could be spoofed - These scenarios should be designed accordingly

Slide 30

Slide 30 text

Smells like an implicit concept that should be made explicit

Slide 31

Slide 31 text

Backend Task (Frontend) Form (Frontend) Represents Is sent to Creates the Task Responds with a Task Provides data for

Slide 32

Slide 32 text

Backend Task (Frontend) Form (Frontend) Represents Is sent to Creates the Task Responds with a Task Provides data for

Slide 33

Slide 33 text

Backend Task (Frontend) Form (Frontend) Represents Is sent to Creates the Task Responds with a Task Provides data for What is it then?

Slide 34

Slide 34 text

Board Velocity Simulated velocity Backlog sprints Current sprint Task draft Columns Change tasks positions Task position Priority

Slide 35

Slide 35 text

Task draft Board Velocity Simulated velocity Backlog sprints Current sprint Columns Change tasks positions Task position Priority

Slide 36

Slide 36 text

Task Draft (Frontend) Backend Task (Frontend) Form (Frontend) Represents Is sent to Creates a Task Responds with a Task Provides data for Permissive Aggregate Validated Aggregate

Slide 37

Slide 37 text

The Validated and the Permissive Aggregates - Frontend aggregates that conform to backend aggregates often need a permissive counterpart - This counterpart represents the intent to create an aggregate - We call this intent a Permissive Aggregate - The domain rules of the Permissive Aggregate are looser - After persisted by the server, we get a Validated Aggregate back - In this scenario: - The Permissive Aggregate only exists in the frontend model - The Validated Aggregate is a concept shared between frontend and backend

Slide 38

Slide 38 text

Permissive Aggregate Validated Aggregate Example implementation of Task and Draft in TypeScript

Slide 39

Slide 39 text

Invalid states

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Frontend validation error

Slide 42

Slide 42 text

Frontend validation error Server validation error

Slide 43

Slide 43 text

Errors in the Permissive Aggregate - Model the Permissive Aggregate as something that support error states as part of its life cycle - This way, this part of the model will fit the frontend domain nicely - E.g.: - A Draft is still in the stage of elaboration, so it can contain errors - A PaymentAttempt may fail, for example, if the payment can't be completed - …

Slide 44

Slide 44 text

How to represent it in code?

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

Implementation guidelines - Prefer plain objects + functions instead of classes - Simplicity and flexibility - Matches the patterns of the community - Works better when we… - Use immutability - Works better when consumed by the UI and the state management layer - Use TypeScript - Make types and concepts explicit in the code - Collocate types and functions that operate on those types - Similar to how a class would force it to be

Slide 47

Slide 47 text

Holding invalid state - After a Validated Aggregate change, return a Permissive Aggregate - domainOperation(validated) = permissive - Have a function that tells everything that is invalid - Rely on backend validation only when trying to persist - Which leads us to…

Slide 48

Slide 48 text

Repositories

Slide 49

Slide 49 text

Repositories on the frontend - Frontend repository implementations interact with: - APIs (REST, GraphQL, …), the browser (LocalStorage, IndexedDB, …) - When persisting a Permissive Aggregate: - Perform a frontend validation first - If everything is fine, send the request, performing a backend validation - What we get back is a Validated Aggregate instance - Repositories are the place to assign this responsibility

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

Validated Aggregate

Slide 52

Slide 52 text

But…

Slide 53

Slide 53 text

APIs are (usually) way more restrictive than databases

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

Databases and repositories HTTP APIs and repositories POST /api/tasks/:id/start and PATCH /api/tasks/:id

Slide 56

Slide 56 text

Databases and repositories HTTP APIs and repositories POST /api/tasks/:id/start and PATCH /api/tasks/:id ?

Slide 57

Slide 57 text

A wrong repository implementation that just proxies the API

Slide 58

Slide 58 text

A wrong repository implementation that just proxies the API The repository should not be an API client

Slide 59

Slide 59 text

Interacting with restrictive APIs - When the relation with the API is a partnership or C/S, this is fine - It becomes a problem if you’re conforming to a external service - Two main scenarios: - More than one endpoint is needed to fetch an Aggregate - GET /api/projects/:id GET /api/projects/:id/settings - A different endpoint is used for different changes of the same Aggregate - POST /api/tasks/:id/start PATCH /api/tasks/:id

Slide 60

Slide 60 text

More than one endpoint to fetch an Aggregate 1. Check your model ‐ Is it actually a single Aggregate? - If it’s not, refine your model and design your code accordingly 2. Check the relation between the frontend BC and the API's - Can the API be changed to accommodate the needs of the frontend? 3. Maybe you will really need two requests - If one of the requests fails, can it be retried? - Are there safe fallbacks that won’t hurt the model integrity? 4. If nothing else can be done, it’s ok to throw an error

Slide 61

Slide 61 text

Different endpoint for each change of the Aggregate 1. Is it possible to infer the endpoint solely from the Aggregate data? ‐ If so, encapsulate it the Repository implementation 2. Is it possible to infer the endpoint knowing what changed? ‐ If so, we have two possibilities: a. Use Domain Events to track the changes b. Compare the Aggregate before the change with the new version 1. Keep a copy of every fetched data inside the Repository instance 2. Redux store, or the cache of the fetching library (e.g.: TanStack Query) ‐ Encapsulate it in the Repository implementation

Slide 62

Slide 62 text

How to implement it?

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

Accept when a repository is not the answer - If a repository effectively adds more complexity - Or just won’t fit nicely in your domain model… - Maybe a repository is not the answer for this scenario - Consider designing a “external” Application Service to encapsulate the access to the other bounded context. For example: - An interface: TasksService - An implementation: HttpTasksService

Slide 67

Slide 67 text

Context mapping

Slide 68

Slide 68 text

Context mapping - Most production-grade frontend applications depend on at least one backend - So context mapping is a constant when using DDD on the frontend - Most frontend bounded contexts are either: - In a Partnership relation - At the downstream side of any other type of relation

Slide 69

Slide 69 text

Project management tool context map Shared kernel Customer/Supplier Customer/Supplier Board (Frontend) D D Tasks Teams & Projects D U Identity & Access U U Partnership Customer/Supplier

Slide 70

Slide 70 text

Discover and implement - Discover what are the proper relations between the frontend and the other bounded contexts - Use repositories and application services to express these relations and the translations between the bounded contexts - Use the mapping to avoid the frontend from being corrupted by: - The API domain model, variable/attributes casing, HTTP status of the API, … - Take care not to implement every relation as if the frontend was always Conformist

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

Additional insights

Slide 73

Slide 73 text

- Plays the same role as they would in a model on the backend - Don't make the frontend be responsible for preserving a server domain model, move system-wide events to the server - If possible, use the same mechanism used by your repositories and services to track changes - E.g.: dispatch events as Redux actions after the persistence is confirmed - If a domain event needs to be translated into a system event, try to also use the same mechanism as the rest of the events - E.g.: if using Redux, use the Redux Toolkit's Listener API Domain events

Slide 74

Slide 74 text

- Backend use cases are usually transactional - But on the frontend, use cases are stateful - Try to reason about the frontend use cases as sagas or state machines: - Between each state, there is user interaction that generates events - The Permissive Aggregate is used as (part of) the state of the use case Stateful use cases

Slide 75

Slide 75 text

Task info is input "Create new Task" use case User clicks Save button The end. User clicks +Task button

Slide 76

Slide 76 text

Task info is input "Create new Task" use case User clicks Save button The end. User clicks +Task button

Slide 77

Slide 77 text

Task info is input "Create new Task" use case User clicks Save button The end. User clicks +Task button * Oversimplification

Slide 78

Slide 78 text

Example frontend use case implementation

Slide 79

Slide 79 text

Example adapter implementation for React

Slide 80

Slide 80 text

- Implement the use case logic isolated from the UI - Then adapt it to the UI making the lifecycle and established patterns of the UI technology as the engine of the use case flow - Eg.: - React + useState + custom hooks - Vue + Reactivity API - Flutter + BLoC - Leave reactivity and state management for the UI layer to implement Implementing use cases on the frontend

Slide 81

Slide 81 text

And then…

Slide 82

Slide 82 text

The rest is canonical knowledge - Some parts of your bounded context only exist on the frontend - Model and design these the way you would do for anything else - You don't need microfrontends to have multiple bounded contexts on the frontend just like you don't need microservices to have multiple bounded contexts on the backend - Of course DDD won’t ignore technical limitations - But it’s not a reason to think that DDD on the frontend is a completely different thing

Slide 83

Slide 83 text

Takeaways - Talk to domain experts and the UX team - Discover implicit concepts that should be made explicit - Don't be afraid of creating new domain concepts - Discover your domain model by separating UI behavior from visual implementations of domain rules - Protect domain code from technical limitations - But accept when technical limitations cannot be circumvented - Use canonical knowledge, but adapt when necessary

Slide 84

Slide 84 text

Thank you, DDD Europe! @talyssonoc | @codeminer42