Learning Domain-Driven Design

My personal notes on the book Learning Domain-Driven Design, by Vlad Khononov. Learning Domain-Driven Design (Vlad Khononov)

Introduction

  • The domain-driven design (DDD) methodology can be divided into two main parts: strategic design and tactical design.
  • The strategic aspect of DDD deals with answering the questions of “what?” and “why?”: what software we are building and why we are building it.
  • The tactical part is all about the “how”: how each component is implemented.

Analyzing Business Domains

  • To design and build an effective solution, you have to understand the problem.

  • What Is a Business Domain? A business domain defines a company’s main area of activity. Generally speaking, it’s the service the company provides to its clients.
  • What Is a Subdomain? To achieve its business domain’s goals and targets, a company has to operate in multiple subdomains. A subdomain is a fine-grained area of business activity. All of a company’s subdomains form its business domain: the service it provides to its customers.
  • The subdomains have to interact with each other to achieve the company’s goals in its business domain.
  • Domain-driven design distinguishes between three types of subdomains: core, generic, and supporting.
    • A core subdomain is what a company does differently from its competitors. To maintain a competitive advantage, core subdomains involve inventions, smart optimizations, business know-how, or other intellectual property. A core subdomain that is simple to implement can only provide a short-lived competitive advantage. Therefore, core subdomains are naturally complex.
    • Generic subdomains are business activities that all companies are performing in the same way. Like core subdomains, generic subdomains are generally complex and hard to implement. However, generic subdomains do not provide any competitive edge for the company.
    • Supporting subdomains support the company’s business. However, contrary to core subdomains, supporting subdomains do not provide any competitive advantage.
  • Since core subdomains’ requirements are expected to change often and continuously, the solution must be maintainable and easy to evolve. Thus, core subdomains require implementation of the most advanced engineering techniques.
  • From a staffing perspective, supporting subdomains do not require highly skilled technical aptitude and provide a great opportunity to train up-and-coming talent. Save the engineers on your team who are experienced in tackling complex challenges for the core subdomains. Finally, the simplicity of the business logic makes supporting subdomains a good candidate for outsourcing.
  • We can use the definition of “subdomains as a set of coherent use cases” as a guiding principle for when to stop looking for finer-grained subdomains. guiding principle for when to stop looking for finer-grained subdomains. These are the most precise boundaries of the subdomains.
  • When looking for subdomains, it’s important to identify business functions that are not related to software, acknowledge them as such, and focus on aspects of the business that are relevant to the software system you are working.
  • Domain experts are subject matter experts who know all the intricacies of the business that we are going to model and implement in code. The domain experts are neither the analysts gathering the requirements nor the engineers designing the system. Domain experts represent the business. As a rule of thumb, domain experts are either the people coming up with requirements or the software’s end users. The software is supposed to solve their problems.

Discovering Domain Knowledge

  • Subdomains are finer-grained problem domains whose goal is to provide solutions for specific business capabilities.
  • It’s crucial for us to understand domain experts and to use the same business terminology they use.
  • To be effective, the software has to mimic the domain experts’ way of thinking about the problem- their mental models.
  • A software project’s success depends on the effectiveness of knowledge sharing between domain experts and software engineers.
  • Using a ubiquitous language is the cornerstone practice of domain-driven design. The idea is simple and straightforward: if parties need to communicate efficiently, instead of relying on translations, they have to speak the same language.
  • Instead of continuously translating domain knowledge, domain-driven design c alls for cultivating a single language for describing the business domain: the ubiquitous language. All project-related stakeholders - software engineers, product owners, domain experts, UI/UX designers - should use the ubiquitous language when describing the business domain. Most importantly, domain experts must be comfortable using the ubiquitous language when reasoning about the business domain; this language will represent both the business domain and the domain experts’ mental models. Only through the continuous use of the ubiquitous language and its terms can a shared understanding among all of the project’s stakeholders be cultivated.ties need to communicate efficiently, instead of relying on translations, they have to speak the same language.
  • It’s crucial to emphasize that the ubiquitous language is the language of the business. As such, it should consist of business domain-related terms only.
  • The ubiquitous language must be precise and consistent. It should eliminate the need for assumptions and should make the business domain’s logic explicit.
  • A model is a simplified representation of a thing or phenomenon that intentionally emphasizes certain aspects while ignoring others. Abstraction with a specific use in mind.
  • Glossaries are best used in tandem with other tools that are better suited to capture the behavior;
  • Knowledge is to converse with domain experts. Quite often, the most important knowledge is tacit. It’s not documented or codified but resides only in the minds of domain experts.

Managing Domain Complexity

  • The solution in domain-driven design is trivial: divide the ubiquitous language into multiple smaller languages, then assign each one to the explicit context in which it can be applied: its bounded context.
  • In a sense, terminology conflicts and implicit contexts are an inherent part of any decent-sized business. With the bounded context pattern, the contexts are modeled as an explicit and integral part of the business domain.
  • Bounded contexts define the applicability of a ubiquitous language and of the model it represents. They allow defining distinct models according to different problem domains. In other words, bounded contexts are the consistency boundaries of ubiquitous languages. A language’s terminology, principles, and business rules are only consistent inside its bounded context.
  • Defining the scope of a ubiquitous language- its bounded context- is a strategic design decision. Boundaries can be wide, following the business domain’s inherent contexts, or narrow, further dividing the business domain into smaller problem domains.
  • Models shouldn’t necessarily be big or small. Models need to be useful.
  • Bounded contexts serve not only as model boundaries but also as physical boundaries of the systems implementing them. Each bounded context shoul d be implemented as an individual service/project, meaning it is implemented, evolved, and versioned independently of other bounded contexts.
  • A bounded context should be implemented, evolved, and maintained by one team only. No two teams can work on the same bounded context.
  • A model should omit the extraneous information irrelevant to the task at hand.

Integrating Bounded Contexts

  • Not only does the bounded context pattern protect the consistency of a ubiquitous language, it also enables modeling. You cannot build a model without specifying its purpose - its boundary. The boundary divides the responsibility of languages. Moreover, models in different bounded contexts can be evolved and implemented independently.
  • There will always be touchpoints between bounded contexts. These are called contracts. The need for contracts results from differences in bounded contexts’ models and languages. Since each contract affects more than one party, they need to be defined and coordinated.
  • After analyzing the integration patterns between a system’s bounded contexts, we can plot them on a context map. The following patterns define different ways bounded contexts can be integrated:
    • Partnership: bounded contexts are integrated in an ad hoc manner.
    • Shared kernel: two or more bounded contexts are integrated by sharing a limited overlapping model that belongs to all participating bounded contexts.
    • Conformist: the consumer conforms to the service provider’s model.
    • Anticorruption layer: the consumer translates the service provider’s model into a model that fits the consumer’s needs.
    • Open-host service: the service provider implements a published language - a model optimized for its consumers’ needs.
    • Separate ways: it’s less expensive to duplicate particular functionality than to collaborate and integrate it.

Implementing Simple Business Logic

  • Transaction Script organizes business logic by procedures where each procedure handles a single request from the presentation.
  • Each operation should either succeed or fail but can never result in an invalid state.
  • The transaction script pattern is a foundation for the more advanced business logic implementation patterns
  • Although business data is important and the code we design and build should protect its integrity, there are cases in which a pragmatic approach is more desirable. Especially at high levels of scale, there are cases when data consistency guarantees can be relaxed.

Tackling Complex Business Logic

Domain Model

  • The domain model pattern is intended to cope with cases of complex business logic. Here, instead of CRUD interfaces, we deal with complicated state transitions, business rules, and invariants: rules that have to be protected at all times.
  • Value objects are implemented as immutable objects.
  • When to use value objects? The simple answer is, whenever you can. Not only do value objects make the code more expressive and encapsulate business logic that tends to spread apart, but the pattern makes the code safer.
  • From a business domain perspective, a useful rule of thumb is to use value objects for the domain’s elements that describe properties of other objects.
  • Entities are the opposite of a value object. It requires an explicit identification field to distinguish between the different instances of the entity.
  • Contrary to value objects, entities are not immutable and are expected to change.
  • An aggregate is an entity: it requires an explicit identification field and its state is expected to change during an instance’s lifecycle. However, it is much more than just an entity. The goal of the pattern is to protect the consistency of its data.
  • The aggregate’s logic has to validate all incoming modifications and ensure that the changes do not contradict its business rules.
  • An aggregate’s public interface is responsible for validating the input and enforcing all of the relevant business rules and invariants.
  • In its simplest form, an aggregate should hold a version field that will be incremented after each update: when committing a change to the database, we have to ensure that the version that is being overwritten matches the one that was originally read.
  • A change to an aggregate’s state can only be committed individually, one aggregate per database transaction.
  • We don’t use entities as an independent pattern, only as part of an aggregate.
  • To support changes to multiple objects that have to be applied in one atomic transaction, the aggregate pattern resembles a hierarchy of entities, all sharing transactional consistency. The- hierarchy contains both entities and value objects, and all of them belong to the same aggregate if they are bound by the domain’s business logic. That’s why the pattern is named “aggregate”: it aggregates business entities and value objects that belong to the same transaction boundary.
  • Principle for designing an aggregate’s boundaries. Only the information that is required by the aggregate’s business logic to be strongly consistent should be a part of the aggregate.
  • Since an aggregate represents a hierarchy of entities, only one of them should be designated as the aggregate’s public interface - the aggregate root.

Domain events

  • A domain event is a message describing a significant event that has occurred in the business domain.
  • Since domain events describe something that has already happened, their names should be formulated in the past tense. The goal of a domain event is to describe what has happened in the business domain and provide all the necessary data related to the event.
  • Domain events are part of an aggregate’s public interface. An aggregate publishes its domain events.

Domain services

  • Sooner or later, you may encounter business logic that either doesn’t belong to any aggregate or value object, or that seems to be relevant to multiple aggregates. In such cases, domain-driven design proposes to implement the logic as a domain service. A domain service is a stateless object that implements the business logic. In the vast majority of cases, such logic orchestrates calls to various components of the system to perform some calculation or analysis.

Modeling the Dimension of Time

  • The event-sourced domain model uses the event sourcing pattern to manage the aggregates’ states: instead of persisting an aggregate’s state, the model generates domain events describing each change and uses them as the source of truth for the aggregate’s data.
  • The event sourcing pattern introduces the dimension of time into the data model. Instead of the schema reflecting the aggregates’ current state, an event sourcing-based system persists events documenting every change in an aggregate’s lifecycle.
  • For the event sourcing pattern to work, all changes to an object’s state should be represented and persisted as events. These events become the system’s source of truth (hence the name of the pattern).
  • The database that stores the system’s events is the only strongly consistent storage: the system’s source of truth. The accepted name for the database that is used for persisting events is event store.

Architectural Patterns

  • Architectural patterns introduce organizational principles for the different aspects of a codebase and present clear boundaries between them: how the business logic is wired to the system’s input, output, and other infrastructural components.
  • Layered Architecture is one of the most common architectural patterns. It organizes the codebase into horizontal layers, with each layer addressing one of the following technical concerns: interaction with the consumers, implementing business logic, and persisting the data.
    • The layers are integrated in a top-down communication model: each layer can hold a dependency only on the layer directly beneath it.
  • Ports & Adapters architecture addresses the shortcomings of the layered architecture and is a better fit for implementation of more complex business logic.
    • The core goal of the ports & adapters architecture is to decouple the system’s business logic from its infrastructural components.
    • Instead of referencing and calling the infrastructural components directly, the business logic layer defines “ports” that have to be implemented by the infrastructure layer. The infrastructure layer implements “adapters”: concrete implementations of the ports’ interfaces for working with different technologies.
  • Command-Query Responsibility Segregation (CQRS) pattern is based on the same organizational principles for business logic and infrastructural concerns as ports & adapters.
    • The CQRS pattern provides the possibility of materializing projected models into physical databases that can be used for flexible querying options.
    • As the name suggests, the pattern segregates the responsibilities of the system’s models. There are two types of models: the command execution model and the read models.
    • Command execution model CQRS devotes a single model to executing operations that modify the system’s state (system commands). This model is used to implement the business logic, validate rules, and enforce invariants.
    • Read models (projections) The system can define as many models as needed to present data to users or supply information to other systems. A read model is a precached projection.

Communication Patterns

  • A bounded context is the boundary of a model - a ubiquitous language. There are different patterns for designing communication across different bounded contexts.
  • In a customer-supplier relationship, the balance of power tips toward either the upstream (supplier) or the downstream (consumer) bounded context. Suppose the downstream bounded context cannot conform to the upstream bounded context’s model. In this case, a more elaborate technical solution is required that can facilitate communication by translating the bounded contexts’ models.
  • The model’s translation logic can be either stateless or stateful. Stateless translation happens on the fly, as incoming (OHS) or outgoing (ACL) requests are issued, while stateful translation involves a more complicated translation logic that requires a database.
  • Stateless Model Translation: the bounded context that owns the translation (OHS for upstream, ACL for downstream) implements the proxy design pattern to interject the incoming and outgoing requests and map the source model to the bounded context’s target model.
  • Stateful Model Translation: when the translation mechanism has to aggregate the source data or unify data from multiple sources into a single model, a stateful translation may be required.
  • Aggregating incoming data: when a bounded context is interested in aggregating incoming requests and processing them in batches for performance optimization. Another common use case for aggregation of source data is combining multiple fine-grained messages into a single message containing the unified data,
  • In some use cases, you can avoid implementing a custom solution for a stateful translation by using off-the-shelf products; for example, a stream-process platform (Kafka, AWS Kinesis, etc.), or a batching solution.

Integrating Aggregates

  • The outbox pattern ensures reliable publishing of domain events using the following algorithm:
    1. Both the updated aggregate’s state and the new domain events are committed in the same atomic transaction.
    2. A message relay fetches newly committed domain events from the database.
    3. The relay publishes the domain events to the message bus.
    4. Upon successful publishing, the relay either marks the events as published in the database or deletes them completely.
    5. The publishing relay can fetch the new domain events in either a pull-based or push-based manner.
  • The outbox pattern guarantees delivery of the messages at least once: if the relay fails right after publishing a message but before marking it as published in the database, the same message will be published again in the next iteration.

Saga

  • One of the core aggregate design principles is to limit each transaction to a single instance of an aggregate, but there are cases when you have to implement a business process that spans multiple aggregates.
  • Co-locating the entities in the same aggregate boundary would definitely be overkill, as these are clearly different business entities that have different responsibilities and may belong to different bounded contexts. Instead, this flow can be implemented as a saga.
  • A saga is a long-running business process in terms of transactions: a business process that spans multiple transactions. The transactions can be handled not only by aggregates but by any component emitting domain events and responding to commands.
  • The saga listens to the events emitted by the relevant components and issues subsequent commands to the other components. If one of the execution steps fails, the saga is in charge of issuing relevant compensating actions to ensure the system state remains consistent.
  • Although the saga pattern orchestrates a multicomponent transaction, the states of the involved components are eventually consistent, i.e., no two transactions can be considered atomic.

Process Manager

  • The process manager pattern is intended to implement a business-logic-based process. It is defined as a central processing unit that maintains the state of the sequence and determines the next processing steps.
  • As a simple rule of thumb, if a saga contains if-else statements to choose the correct course of action, it is probably a process manager. Another difference between a process manager and a saga is that a saga is instantiated implicitly when a particular event is observed. A process manager, on the other hand, cannot be bound to a single source event. Instead, it’s a coherent business process consisting of multiple steps. Hence, a process manager has to be instantiated explicitly.
  • From an implementation perspective, process managers are often implemented as aggregates, either state based or event sourced.

Design Heuristics

  • Refactoring logical boundaries is considerably less expensive than refactoring physical boundaries. Hence, when designing bounded contexts, start with wider boundaries. If required, decompose the wide boundaries into smaller ones as you gain domain knowledge.
  • The knowledge of both the business logic implementation pattern and the architectural pattern can be used as a heuristic for choosing a testing strategy for the codebase.
  • Tactical Design Decision Tree: the business logic patterns, architectural patterns, and testing strategy heuristics can be unified and summarized with a tactical design decision tree.

Event-Driven Architecture

  • Careless application of EDA can turn a modular monolith into a distributed big ball of mud.
  • Event-driven architecture is an architectural style in which a system’s components communicate with one another asynchronously by exchanging event messages.
  • There are two types of messages:
    • Event: a message describing a change that has already happened.
    • Command: a message describing an operation that has to be carried out.
  • Since an event describes something that has already happened, an event’s name should be formulated in the past tense.
  • Types of Events
    • Event notification is a message regarding a change in the business domain that other components will react to; it should not be verbose.
    • Event-carried state transfer (ECST) messages notify subscribers about changes in the producer’s internal state. It includes all the data reflecting the change in the state. ECST messages can come in two forms.
    • Domain events are somewhere between event notification and ECST messages: they both describe a significant event in the business domain, and they contain all the data describing the event. They are intended to model and describe the business domain.