Marcin Jahn | Dev Notebook
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Garbage Collection
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Memory Management
      • Memory Management
      • Garbage Collector Internals
      • GC Memory Layout & Allocation
      • GC Advanced Topics
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Unknown
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the HTML & CSS section HTML & CSS
    • HTML Foundations
    • CSS Foundations
    • Responsive Design
    • CSS Tips
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • Microservices
    • MapReduce
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Garbage Collection
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Memory Management
      • Memory Management
      • Garbage Collector Internals
      • GC Memory Layout & Allocation
      • GC Advanced Topics
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Unknown
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the HTML & CSS section HTML & CSS
    • HTML Foundations
    • CSS Foundations
    • Responsive Design
    • CSS Tips
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • Microservices
    • MapReduce

Domain-Driven Design

The Domain-Driven Design (DDD) is an architecture style for building software systems where business requirements are clearly stated and separated from the surrounding application or infrastructure concerns. Some argue that microservice architecture is true only with DDD, although that might be a bit of an overstatement in my opinion.

The main goal of DDD is to make software more maintainable, to make it less complex to deal with. Its focus on the Domain requires a bit of a shift in how we think of software development. Using DDD might actually help to “discover” the domain properly, highlighting key entities (aggregate roots) and their invariants, together with bounds of these entities.

In this article, I will assume the use of Clean Architecture (or Hexagonal), because I believe it composes perfectly with DDD.

AI

You might question the value of learning these concepts in the age of so-called “AI”. I’d argue that the Domain part of your systems (especially Aggregates and Entities) should not be written by LLMs. It’s the most crucial part of the bounded context you’re working on, and its proper design not only increases the chances of your solution to do its job as it’s supposed to, but the process of creating the Domain layer enhances your understanding of the process as well. Often, when we’re tasked with creating a solution, the domain is not fully defined, or understood. Leaving the decisions to LLMs will result in random invariants.

Having said that, LLMs are much better suited to work on Infrastructure, Application or Presentation layers. There will be much more boilerplate that is not that pleasant to work on.

Bounded Context and Sub-Domains

As part of DDD, we identify Subdomains that take part in our system. Subdomain is a subset of some larger Domain. The terms are rather fuzzy. Every subdomain is some domain itself after all. Domains and subdomains are a hierarchical concept.

When we design DDD-compliant system, the domain is the general problem we’re trying to solve, let’s say “Monitoring of IoT Platform Instances”. This great domain may be splitted into smaller concerns - subdomains:

  • users management
  • platforms management
  • notifications
  • etc.

A Bounded Context represents a linguistic boundary within or across subdomains. Often, a Bounded Context is aligned with a Subdomain. The difference between these two concepts is as follows:

  • a Subdomain is in the Problem Space.
  • a Bounded Context is in the Solution Space.

So, a given bounded context is a proposed solution to a problem within some subdomain(s). Ideally, one bounded context should cover one sub-domain. For larger sub-domain we could have many bounded contexts covering it. The relation between Sub-domain and Bounded Context is a bit fuzzy then.

In a typical project, there will be many bounded contexts. Ideally, each of them should be developed by a different team. Each would also use a separate persistence data store.

Bounded Context is made up of all the aspects that are drived by its model:

  • the domain model itself
  • the application that uses the model
  • database schema that persists the model objects

Bounded Context vs Microservice

There isn’t a 1:1 relation between a bounded context and a microservice. A single bounded context could be served by multiple microservices. For example, one microservice could be in a form of an HTTP API, serving requests. Another microservice could be a listener on some bus. Both could work on the same database, but they’d work on a different parts of the overall responsibility of the bounded context. However, they’d both use on Ubiquitous Language set, and a single Domain layer.

Ubiquitous Language

The most important part of DDD is the establishment of the Ubiquitous Language - an agreed vocabulary (between developers and domain experts) that will be used to describe various entities in the Bounded Contexts. It should be used everywhere in the project: code, diagrams, discussions, etc.

Every bounded context will have its own ubiquitous language.

Context Maps

The namings in different Bounded Contexts and their relations is what defines Context Maps. It could be that different Bounded Contexts use the same name for different entities. It could also happen that different bounded context would model the same entity differently (like a Customer in Appointment Scheduler context and Customer in the Billing context).

A Context Map clearly shows what a given entity is represented by in another context.

Synchronization

Different bounded context may model data for the same physical entity. For example, Patient Management context and scheduling context may contain the Patient entity. Probably, the one in the Patient Management context will be more detailed. Also, probably that one will allow for various modifications. E.g., we could change the patient’s name. The other contexts that model the Patient should be notified about this change. E.g., it could be done via some message bus. The Patient Management service would publish a message about the change, and all other interested services would be listeners.

This approach obviously complicates the overall system. It also introduces Eventual Consistency (which really is unavoidable in distributed systems). Therefore, it’s important to consider what data is truly needed in a given bounded context. When referencing entities from different bounded context, the best case scenario is to reference it just by its ID. IDs are immutable and they will not change. Any additional data (that can change in time) will probably require us to setup the synchronization mechanism (via Domain Events). Note that referencing just the ID might also not solve the problem entirely. We might have a case where a given entity gets removed from the other Bounded Context. Its removal might need to be propagated to other bounded contexts as well.

Domain Model

Designing our domain model is crucial. According to Eric Evans:

The domain is the heart of business software.

Another point is that models evolve over time. Our initial assumptions can be often invalidated as we progress in understanding the domain.

In our modeling, we should focus on the behaviours of the models. To find such behaviours, we need to look at all the possible events that may occur in the system - those events are basically the use-cases that the solution is expected to fulfill. Some examples of these events could be (in medical clinic system):

  • add a new patient
  • schedule a visit
  • move a visit to another date

Rich Domain Models

DDD encourages the use of Rich Domain Models, opposed to the Anemic Domain Models. Anemic models are simply classes that are DTOs, or classes with very little logic inside of them.

Often in our programs we have DTO classes and other service classes that act on these DTOs, potentially modifying them. This is an anti-pattern in the DDD world. Martin Fowler argues that this is even an anti-pattern in the OOP sense, since OOP is supposed to merge data and behaviour together.

Entities

These are objects defined by their identity. That means that every instance of an Entity is unique, it has its own key identifier. That ID should be unique. When a given Entity is contained within some Aggregate Root, its ID must be unique only within its Aggregate Root instance.

Sometimes it might not be that easy to understand whether a givne thing is an Entity or just a Value Object. A good way to figure that out is to consider whether that “thing” has some lifecycle. If there are business cases where the thing gets modified (while upholding its identity), it most likely is an Entity.

Potential candidates for entities in Medical Clinic Management system would be:

  • Patient
  • Doctor
  • Room
  • Appointment

Some of those might even be Aggregate Roots.

An entity should always be in the valid state (to uphold its invariants). Hence any modification of an entity (or its creation) should contain various guard clauses that make sure the operation can be done. For example, before I rename a Patient, I should make sure that the new name is not empty.

Entities also contain Domain Events. These may be used to inform other parts of the system of changes.

Value Objects

Identity of Value Objects is based on composition of its values. They’re immutable. Whenever some part of value object chages, a new object should get created, while the old one gets disposed. Value objects may contain methods (without side-effects). Value Objects represent things that are not defined in the domain. They rather describe/measure/quantify something in the domain somehow. Examples include:

  • money
  • date range

An instance of a value object does not represent any unique entity, it’s just a set of information representing something in our domain.

It’s recommended to first consider Value Object when deciding whether to use Entity or Value Object for a given thing.

Identifiers

In some projects identifiers of Entities are value objects. These are custom types that contain just an identifier (for example as a Guid). So, we could have a type called CustomerIdValueObject. Creating such types (vs using a plain String) helps to uphold invariants about our Ids. Potentially they also help to avoid misassignments of wrong IDs when a given method requires a few of them.

Date types are a great example of value objects.

It is OK for Value Objects to reference Entities!

A quote from Eric Evans:

VALUE OBJECTS can even reference ENTITIES. For example, if I ask an online map service for a scenic driving route from San Francisco to Los Angeles, it might derive a Route object linking L.A. and San Francisco via the Pacific Coast Highway. That Route object would be a VALUE, even though the three objects it references (two cities and a highway) are all ENTITIES.

Entities might change state, while Value Objects shouldn’t, therefore keeping entity (aggreagate root!) within a Value Object is a bit “controversial”. It’s important to treat the contained entity as a reference only, and not a core part of the Value Object itself.

Domain Services

Logic/behaviour that doesn’t fit into Entities or Value Objects goes into separate classes called Domain Services. Such services often deal with different kinds of entities/value objects. For example, they could orchestrate some workflow.

Before creating a service, we should make sure that the logic we’re adding doesn’t fit into any of the existing domain elements (entities/value objects).

Overuse of domain services might lead to anemic models.

Domain Services are often a good artifact to use to avoid placing domain login in the Application layer.

Aggregates

When building our Entities we will often end up with Aggregate entities, that is, entities that are linked with other entities or value objects.

Citing Martin Fowler:

A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it’s useful to treat the order (together with its line items) as a single aggregate.

One Entity should only belong to one Aggregate. One Value Object can belong to many Aggregates.

Just the fact that one entity refers to another (via some property) does not mean that they are a part of the same Aggregate! It could be that some entity belonging to one Aggregate refers to some other entity that is an Aggregate Root of another Aggregate.

An example of that could be the Snack Machine scenario from Domain-Driven Design in Practice (Pluralsight). We had there: Snack Machine -> Slots -> Snacks.

Snack Machine and Slot belonged to one Aggregate (since a Slot cannot exist without a Snack Machine). A Snack was a separate Aggregate.

Aggregate Root

Aggreagate Root is an entry point of an aggregate that “defines” the aggregate as a whole.

It’s easy to design Aggreagate Root improperly or not optimally. Often we arrive at the valid design iteratively, starting from some initial assumptions. As we learn more about the domain (which evolves!), our model will evolve as well. One way to find out what is an aggregate root, we need to look at individual components and think if the removal of a given component would result in removal of its contained components (cascading delete). If it would, that’s probably an Aggregate Root.

Aggregate Root is like a central entity that defines it completely. It should allow us to keep the whole object in a valid state (enforcing invariants). It creates a consistency boundary, within which those invariants are enforced. Aggregate Root should also have transactional consistency. Consistency of things outside of the aggregate is irrelevant.

Consistency Boundary is the reason for Aggregate Roots to exist in the first place!

Aggregater Root shouldn’t be too large (to stay performant, since loading lots of internal entities might be heavy), and should not be too small (to avoid anemic model and failing to protect invariants). We shold prefer smaller aggregates though (as big as they need to be and not bigger). Larger aggregates make consistency boundary and transaction scope large as well.

A simple example of keeping invariants is an Order aggregate root that should make sure the number of Items within it does not cross some business-defined limit.

We should access/modify the aggregate only throught the Aggregate Root. Internal entities (non-agregate roots) should stay private in the Aggregate Root, and they should not be referenced outside of the Aggregate Root (unless it’s temporary within some process execution). Accessing those internal entites should be only via the Aggregate Root instance. Aggreagate Root may also hold reference to some other Aggregate Root (either as a reference to object or just by the ID).

Add more entites to the aggreagate root only when that’s necessary to uphold invariants.

There will also be entities that do not include other entities or value objects. The convention is to call these Aggregate Roots as well.

A single bounded context or domain may contain a few Aggregate Roots.

Transactions

Bounded Context should modify just 1 aggregate root instance per transaction scope! Otherwise, we can easily end up with situations where the system is a single-user system due to lots of concurrency errors.

Note that in some specific cases, upholding that rule might not be possible. In general, it’s important to keep the “best practices” in mind, but also to be able to break them when they just don’t apply to our case.

When a new business requirement comes in that requires you to update a few aggregate roots instances in a single transaction, it is worth to make sure that the model itself is still valid. It might be, that the new business requirement is actually an update to the Ubiquitous Language, and a new Aggregate Root should get introduced.

For example, if we have some Item entity (aggregate root), and we get a requirement to make sure that when updating one Item, we also need to update some other Item, try to discuss with the Domain Expert what this pair of items actually represents. Maybe there should be a new Aggregate Root (like ItemsPair) introduced in the system and Item itself should not be Aggregate Root anymore? With such new Aggregate Root in place, we will manage Items from a different level (new repository), and we can keep the “one aggregate root per transaction” rule alive.

It might also be that the new requirement introduces more issues than it gives benefit. Maybe the requirement should be changed to keep the system maintainable.

There will be cases where one Aggregate Root istance has references to other Aggregate Roots. Even then, we shold try to upkeep the “one transaction per aggregate” rule. Either update the referred aggregate or the reference holder in a transaction, but not both. Use domain events to propagate changes, making sure the whole thing is eventually consistent.

It’s often the case that a requirement of transactional consistency across multiple aggregates may be actually relaxed to eventual consistency.

Transactions should be managed in the Application Layer.

Associations

DDD encourages one-way relations between entities. It’s popular in Entity Framework to define navigation properties in both ways. It turns out it’s not always needed. It makes entities more complex. By default, we should start our modeling with uni-directional relationships and switch to bi-directional ones only when necessary.

It still is OK to keep an identifier of the other entity (like a foreign key) in the “child” entity.

One aggregate should only reference external entities that are aggregate roots. For example, if a Customer has some Address, other aggregates (e.g., Order) should not reference the Address directly. Instead, they should get that address through a Customer.

Aggregate Roots define the complete entity. We should not link some other aggregate to a part of an aggregate.

However, it’s OK to link to some other non-root entity by the identifier.

When thinking to associate two aggregates, it’s worth to remember what defines an Aggregate Root:

We need to look at individual components and think if the removal of a given component would result in removal of its contained components. If it would, that’s probably an Aggregate Root.

If removal of our root should not result in the removal of linked aggregates, possibly we don’t need to include these children as “Navigation Properties”. Instead, maybe just an ID of that other entitiy is enough.

An example is an aggregate called Appointment. It would contain references to:

  • Patient
  • Doctor

However, since removing an Appointment should not remove Patient and Doctor, it makes sense to reference these just by ID, not by Navigation Properites.

Shared Kernel

In DDD, a Shared Kernel is code that is used between different bounded context. It’s basically a kind of project that .NET developers often call Common. The shared part should be as stable as possible. Changes in that code will affect a lot of places.

Repositories

Repository pattern is a well-known approach and it is also used outside of DDD. DDD-specific fact is that only the Aggregate Roots should have their repositories. Other entities should be accessed via their aggregate roots.

There are two ways to create an Aggregate instance:

  • via its constructor/factory method - when instantiating a new instance with a brand new ID
  • via repository - when reconstituing an existing instance that was previously created. It also happens via cosntructor/factory, but from the Application Layer perspective, the repository is the source of the object.

Repository might have methods to return other things than just Aggreagate Root instances. Depending on the businesss cases, we might need things such as:

  • counts of objects
  • some composition of parts of different aggregate root(s) - these compositions would be Value Objects

The second case from the list above is a bit of a code smell though. When we employ such tactics, it might point at some issue with our Domain modeling. It could be that our aggregates are not defined properly. Such approaches are most often used for performance reasons. Even when you decide to add such repository method, make sure that the data that gets exposed is a data that you’d be able to navigate to if you had a reference to the Aggregate Root itself. Otherwise, you might break the consistency boundary.

A repository could use another sub-repository to fetch some additional data.

Domain Events

There are two kinds of events:

  • in-domain - Domain Events (in-process)
  • across domains/bounded contexts - Integration Events

Domain Events

Domain Events are raised when something happens in the domain that could be of interest to the other pieces of our application (but in the same process!). They should be describable in the Ubiquitous Language and their names should correspond to that. The naming should be in the past tense, since the events will always inform about something that has already happened. Some examples:

  • Client Registered
  • Appointment Scheduled

YAGNI

Create events only when there is some use case for it. Don’t create them “just in case”.

In code, each event is a separate class. It should iunclude the set of information that might be interesting for a given event.

Dispatching Events

Events should be dispatched from the Domain layer. Entities or Domain Serves could emit them.


Events may have multiple handlers. Normally, the order of their execution should not matter.

Integration Events

These events are the way to share information that something happens across domains or applications. They could include more information than the Domain Events since the receiver of the event might not be able to get these information on its own. For example, instead of just sharing the ID of some changed entity, we would also share some more defining properties, like a Name. It could also happen that the event handler would have to get back to the source service to ask for more details. We might not want that, especially if there’ll be a lot of events, or a lot of handlers.

Domain and Integration Events

We could have cases where some event has both Domain and Integration Events defined. The entity would publish a Domain Event and one of the handlers would publish an Integration Event, knowing that there might be some handlers interested in this event outside of the domain or app.

Integration Events often use some kind of message bus, like Azure Service Bus.

Anti-Corruption Layer

It often happens that we have to integrate with systems that are outside of our control. Such systems will most likely use a different modeling than ours. In such cases, Anti-Corruption Layers help us to create a kind of mapper between our domains and the other systems. Such layers are basically like Adapters.

It could also work the other way round. We could have a “legacy” system that we want to update to integrate with a “well-designed” DDD project. In order not to introduce the new concepts into legacy codebase, we could create an ACL layer (like some set of services) that will communicate with our DDD system properly and return the data as the legacy system expects.

In practice, ACL is often just a mapper/translator kind of class. In more complex scenarios, it could be a separate process with endpoints that reach out to the legacy system and translate the responses to our Ubiquitous Language.

References

  • Domain-Driven Design: Tackling Complexity in the Heart of Software (by Eric Evans)
  • Implementing Domain-Driven Design (by Vaughn Vernon)
  • DDD on Pluralsight
    • Domain-Driven Design Fundamentals
    • Domain-Driven Design in Practice
    • Refactoring from Anemic Domain Model Towards a Rich One
    • Domain-Driven Design: Working with Legacy Projects
←  What is architecture?
Microservices  →
© 2025 Marcin Jahn | Dev Notebook | All Rights Reserved. | Built with Astro.