DDD, hexagonal architecture and frontend: What is this all about?
During the past weeks, my team mates have created a study group to learn about Domain driven design in order to value it and how to apply it on some legacy code and new epics. On the case of the app team, where I belong, we’ve being digging how to apply it on frontend architectures.
This article contains a summary of concepts related with DDD, hexagonal architecture, so we have things clear about what all this means and relates, as well as a schema of how it would fit on a frontend architecture.
DDD (domain driven design) concepts summary
DDD is focused on building software as the representation of the product in the real world (good model of the business domain).
This is done, first, through strategic design, which is focused on how the project teams work to divide the contexts and make sure the members of a team speak the same language.
Second, through tactical design, related with which technical resources to use on the building of the software.
Normally, hexagonal architecture is used with DDD, but other architectures can be used instead.
When applying only the tactical design, it is called DDD-lite (not recommended).
Note: business logic layer === domain model
Tactical design concepts
Based on Model driven design.
Entities: Unique identifier, changeable object.
Value objects: Immutable, no id, what defines them is its value. You need to create a new one to replace the old one.
- Example 1: a “color” value object.
- Example 2: The “line” elements of an “order”. The line has no id, but the reference to the order id and the SKU value of the product bought. This line can’t be edited: A new line is added to the order or an existing line is deleted from the order.
- Example 3: The StudentPassword from a Student. The validation logic related (minimum length, required characters…) can be encapsulated on a piece of code to avoid code duplication.
Aggregates: Conceptual set of entities and value objects. We treat it as a single unit for the purpose of data changes. The aggregate will be the boundary where we make sure every operation ends in a consistent state. These should be the only entities accessible from repositories (repositories should only return aggregates).
Aggregates collaborate to have a great performance, as they only have the needed properties and do a single query or update to read or write the data we need.
If it needs to protect itself from an invariant (validation rule) it’s an aggregate, otherwise it’s an entity.
- Example 1: A “video” has “comments” and “ratings”, which would make the video class an aggregate.
- Example 2: On a shopping site, the cart is an aggregate as it’s composed of a collection of items that can be treated as a single unit (order, lines).
- Example 3: Look for the invariants (aka validation rules). On an farming project, Field would be the aggregate root, and its seasons (Season) would be part of the aggregate, as the invariant is “the season area can not be greater than the field area”.
Repositories: Where the aggregates or what we need to persist are stored (Ex: SQLite).
Services: We can classify them as:
- Domain services: These services perform domain-specific operations and encapsulate the business logic.
- Application services: These services contain the logic related with the application workflow. External consumers would communicate with our system through the application services.
- Example: We have a User services file where we have methods such as authenticate, fetchUserData, changePassword…
Factories: The factory pattern, they can return an aggregate, entity or value object.
Domain Events: When the state of an entity or aggregate changes, a domain event is triggered, this event will be listened to, and some actions will happen.
- Example: On an app using React Native & MobX, the “reaction” from MobX.
Domain Exceptions: Exceptions can be used to express domain concepts.
- Example: The order can’t be allocated. There are multiple reasons for this, but if we arise an exception called ‘outOfStock’, it’s more helpful.
Modules: Modules help segregate concepts.
- Example: The Java packages or namespaces on C#.
Strategic design concepts
Bounded context: It’s the limit a domain model can be applicable, it’s used to delimitate epics or pieces of work from different teams, this way the teams avoid tripping over each other.
- Example 1: If I have “user” used on two contexts (sales & support), it’s better to create a user class for each one of the contexts, because a context will use X data from user and the other context is not meant to be using the same data. This avoids code conflicts between the teams. This is important to the aggregates and having the logic context close to the data.
- Example 2: It wouldn’t be very correct to consider the app/frontend part as a bounded context but a separate project.
- Example 3: CodelyTV courses platform -> Bounded Contexts: Mooc, Videos module, Courses, Students, BackOffice, Courses, Tickets, Students, Blog
Ubiquitous language: Each bounded context has its own ubiquitous language focused on the terms and concepts of its business/product domain. The whole team needs to agree on these terms and concepts definitions. This is what will be reflected on the code.
Context Maps: It shows the relation among the different bounded contexts of the whole project.
The bounded contexts can be related each other by using database connections in order to avoid code duplication.
Customer / Supplier: It’s the relationship between the client and the server. Both teams are in continuous integration.
- Example 1: The app team and the backend team collaborate: Backend applies changes on their context, and through the API, app team consumes the expected data and applies the needed changes on the app code.
Conformist: It’s the customer / supplier case where the supplier (server) won’t meet the needs of the customer (client) on their context.
- Example 1: The app team and the backend team. There are epics where the backend team can’t provide the perfect changes to the app team because of time or code complexity. On this case, the app team needs to think about a solution that takes advantage of what is already done on backend, which is consumed through the API.
- Example 2: Similar case happens on third parties libraries or on a project context that is not maintained.
Partner: Teams are dependent and need to cooperate to meet the development needs of both contexts.
Anticorruption layer: The client creates an intermediate layer that communicates with the server, but applies its own domain model.
- Example 1: On an app, the serializers rename the model params from backend or don’t save the ones the app doesn’t use.
- Example 2: While doing a refactor, we define this layer so it’s only the infrastructure part affected by the new changes and not the domain part.
Big ball of mud: Segregate or separate code to decouple it from a not well maintained legacy code.
“A big ball of mud is the natural state of software in the same way that wilderness is the natural state of your garden”.
Separate ways: The team on each context can freely use a stack different from the other team/context.
Open host service: Serve an API where different parts of the product which are built with different technologies or stacks, can consume that API and communicate among them (app + backend, for example).
Published language: The project’s API, the project’s official documentation…
Hexagonal architecture concepts summary
Layers know each other from the outside to the inside:
Infrastructure: Here lies the code related with input/output (BD, files) or coupled to an external vendor.
Application: It represents the use cases with the domain naming. It’s like a transactional barrier on events triggering and BD.
Domain: It represents the models of our business (value objects & entities). The domain services are included in this layer too.
How it works
The Domain layer is known by the Application layer, and the Application layer is known by the Infrastructure layer, this way, if we need to make changes on the infrastructure (new framework, ORM…) it won’t affect the rest as we don’t have any coupling.
The petition flow received by the controller of the Infrastructure layer, will be sent to the Application service.
The application service would build the value objects from the primitive data types received from the controller.
From the application service, we will be calling the different services, models and repositories.
The application layer would communicate through an interface of the repository on the Domain layer. The implementation of such interface will be on the Infrastructure layer following the adapter pattern.
Hexagonal architecture applied to frontend projects
Layers organization to not to be coupled to the framework
Domain layer
App layer
Adapter (layer that communicates with app) with Vue or jQuery
This example and screenshots are from the presentation “The art of front end architecture” by Adrià FontCuberta on CodeMotion 2020.
Sources
- What is DDD?
- What is strategical design?
- What is tactical design?
- Architecture patterns with Python
- CodelyTV DDD course
- Codemotion 2020 Adrià Fontcuberta’s presentation about The art of frontend architecture (hexagonal architecture)
Interesting articles
- Domain, subdomain, bounded context, Problem/Solution Space in DDD: Clearly Defined
- Ubiquitous language & the joy of naming