Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[DDD Europe 2024] DDD on the frontend

[DDD Europe 2024] DDD on the frontend

Due to the specificities of frontend applications, modeling the domain in this context has always been challenging. Ranging from generating aggregate ids and designing their lifecycles, to the need to represent invalid states, to context mapping when communicating with vendor APIs, there are a series of factors to be considered when modeling the domain. In this talk, I will show you these specificities and how to tackle everyday frontend complexities using canonical DDD concepts, designing new bounded contexts just for the frontend, while still embracing modern frontend approaches.

More Decks by Talysson de Oliveira Cassiano

Other Decks in Programming

Transcript

  1. 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 - …
  2. Board Velocity Simulated velocity Backlog sprints Current sprint Task draft

    Columns Change tasks positions Task position Priority
  3. 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 - …
  4. 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
  5. 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
  6. 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?
  7. 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
  8. "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
  9. 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
  10. Backend Task (Frontend) Form (Frontend) Represents Is sent to Creates

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

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

    the Task Responds with a Task Provides data for What is it then?
  13. Board Velocity Simulated velocity Backlog sprints Current sprint Task draft

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

    Columns Change tasks positions Task position Priority
  15. 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
  16. 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
  17. 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 - …
  18. 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
  19. 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…
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. - 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
  29. - 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
  30. Task info is input "Create new Task" use case User

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

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

    clicks Save button The end. User clicks +Task button * Oversimplification
  33. - 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
  34. 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
  35. 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