
In the previous post, I presented the project, its motivation and architecture. Now, it’s time to start checking some implementation.
- Part 1 - Project’s overview
- Part 2 - Strategic and Tactical Design
- Part 3 - Domain events and Event Sourcing
- Part 4 - Implementing Event Sourcing with MartenDB
- Part 5 - Wrapping up infrastructure (Docker, API gateway, IdentityServer, Kafka)
- Part 6 - Angular SPA and API consumption
Ubiquitous language
It depends on the context
Before jumping into Strategic
and Tactical
Design, you must know that Domain-driven design is ineffective if you’re unable to accurately describe the domain you’re modeling. For that, you need a particular language; the domain language, as known in DDD as ubiquitous language, which describes everything in and out of your domain implementation, the behaviors and outcomes.
This vocabulary has to be provided by a domain expert
, actively working with developers so they can write the code using the same terminology and understand the workflow of the business in depth. It needs intensive collaboration and knowledge exchange. Over time, developers master the domain intricacies and become very knowledgeable in that niche. Commonly, companies hire developers experienced in a specific domain.
When working with domain experts, I find it very practical to document this language through a glossary of the domain dialects and terms they use and reach out to it as much as possible before coding. Depending on the level of experience working in that field, if seen from an outsider’s perspective, developers may look like domain experts themselves. Ultimately, knowledgeable developers should be able to transfer the business concepts to joiners with ease, and that’s a powerful metric to validate if the overall design is correct.
The setting in which a word or a statement appears that determines its meaning
To effectively set the mind in the domain-driven design using the ubiquitous language
, as developers, we also need to accept that the same name can have multiple meanings, which is challenging because we correlate good code with wide reusability. Suppose you’re a die-hard DRY
(Don’t Repeat Yourself) practitioner, you’ll be tempted to abstract everything you consider common and end up with generic models or services to be shared across the project without respecting boundaries, and that can lead to not only high coupling but also misconceptions, under this domain-centric approach, so be careful.
Knowing that the vocabulary will change the meaning depending on the context, you may duplicate things here and there, and that’s completely fine sometimes.
Bounded contexts
Along with the ubiquitous language, Bounded contexts
are a core element of strategic design
. They establish clear boundaries where a specific domain model applies, allowing different parts of a system to have distinct and even conflicting models—each valid within its own context.
For this project, I defined the following bounded contexts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── CustomerManagement:
| Manages customer-related information.
├── InventoryManagement
| Tracks availability of products in the warehouse.
├── ProductCatalog
| Manages product-related information, such as price and description.
├── QuoteManagement
| Manages the creation, processing, and tracking of price quotes for customers.
├── OrderProcessing
| Deals with the processing of orders, from placement to fulfillment.
├── PaymentProcessing
| Manages the processing of payments for customer orders.
└── ShipmentProcessing
Manages the logistics for delivering the order.
Core domain and Subdomains
As the name suggests, the core domain represents the heart of the business, and it’s the very reason why it exists. Most of the attention should be on designing this core domain strategically and technically very well since it correlates with a competitive advantage in the market where the product is made for.
With the structure above, I could say that the OrderProcessing
is the Core domain
, once the central focus of this system is processing customer orders, while it depends on supporting Subdomains
to fulfill the flow that started when an order is placed, such as Product Catalog
, Inventory Management
, Payment Processing
, until it’s delivered through Shipping Processing
, for example.
Defining subdomains within limited boundaries can be handy, especially when dealing with complex projects; where the domain is too abstract, you must divide it into smaller pieces to conquer. Usually, it grows as granular as you learn about the domain itself. It’s important to mention that the definition of domain and subdomain will vary depending on the project and its business goal.
Note: Keep in mind that the domain representation in this project is naive and inaccurate, with no pretension to look like a real e-commerce solution or its departments.
Strategic and Tactical Design
For implementing DDD as the development approach, you must use both Strategic
and Tactical
design concepts to streamline the design from different aspects and concerns.
The Strategic design
focus on the high-level concepts for the whole solution, such defining the Bounded contexts
, Context mapping, and Subdomains. This will define the overall organization of the entire system.
The Tactical design
is concerned in defining the details of the Aggregates, Value Objects, Entities, Repositories, Services, etc.
Although I won’t cover domain mapping in this series, different techniques can be used to drive this design, including Event storming, a workshop-based session aiming to explore the domain intrisecaties to the light right from the domain experts
experts in collaboration with developers, stakeholders and other roles, simillarily to brainstorming
.
Note: Event storming
, Domain Events
and Event sourcing
are different things, although they have to be used together for a more comprehensive approach of the system design.
Do Microservices limit boundaries?
When looking at the project architecture, it’s important not to assume that each microservice maps one-to-one with a bounded context. In fact, a single bounded context may contain multiple services, and DDD is not prescriptive about microservice boundaries.
For the sake of simplicity in this project, I only needed a microservice per bounded context, but once the csproj
has a similar name of the bounded context, I felt I needed to clarify that relation to avoid confusion. These are the namespaces representing each bounded context, with their respective microservice inside:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── Services
│ ├── CustomerManagement
| | └── EcommerceDDD.CustomerManagement
│ ├── InventoryManagement
| | └── EcommerceDDD.InventoryManagement
│ ├── ProductCatalog
| | └── EcommerceDDD.ProductCatalog
│ ├── QuoteManagement
| | └── EcommerceDDD.QuoteManagement
│ ├── OrderProcessing
| | └── EcommerceDDD.OrderProcessing
│ ├── PaymentProcessing
| | └── EcommerceDDD.PaymentProcessing
│ └── ShipmentProcessing
│ └── EcommerceDDD.ShipmentProcessing
Important: Microservices have nothing to do with DDD. DDD isn’t concerned with architectural styles.
Hands-on Tactical Design
Before diving into the domain implementations, the tactical design across the solution relies on a set of reusable abstractions defined in a shared core layer.
Take a look at the EcommerceDDD.Core
project, specifically the Domain
namespace where I placed all the foundational building blocks needed for modeling each bounded context. This layer is intentionally abstract and reusable, featuring types like AggregateRoot
, Entity
, ValueObject
, and more.
Since there’s no domain-specific logic here, it serves as a clean, domain-agnostic abstraction — a good example of DRY. In fact, it could easily be packaged as a NuGet library to support other projects.
What are Aggregate, Aggregate Root, and Value Object?
In Domain-Driven Design, an Aggregate
is a group of related domain objects that form a consistency boundary and they are modified together and treated as a single unit.
At the center of this group is the Aggregate Root
, a primary entity that acts as the entry point for accessing or modifying anything inside the aggregate. External objects are only allowed to interact with the root, which ensures domain rules and consistency are enforced.
On the other hand, a Value Object
is an immutable type that models a descriptive aspect of the domain without a unique identity. Its equality is based on its content rather than a reference. Value objects encapsulate logic and validation, making them ideal for modeling concepts like money, quantities, and dates.
Now let’s examine the Product
aggregate root, located in the ProductCatalog
bounded context — the simplest one in this project. I chose it because it’s the only context not using Event Sourcing. And that’s one of the strengths of microservice architecture: you don’t need to enforce the same patterns everywhere. Overusing Event Sourcing is an anti-pattern you should avoid when it’s not justified.
As you progress through the series, you’ll see how DDD is not just about modeling but about placing behavior and business logic directly into domain objects, moving away from the traditional anemic model.
Here’s an example of a value object used by the Product
aggregate:
🔑 Key Characteristics of the Product
Aggregate Root
These are some tactical patterns I consistently apply to all aggregate roots in the solution:
Product
is the aggregate root for its bounded context.- It uses a strongly typed value object (
ProductId
) as its identifier. - It encapsulates entities and value objects, exposing behavior through controlled methods.
- All fields use private setters to ensure invariants are maintained.
- Object creation is always validated via a static
Create
method that acts as a factory and enforces rules. - The constructor is private, preventing the creation of invalid objects from outside.
Avoiding God models
You may notice that I have duplicated Money
and Currency
value objects across different bounded contexts. This is intentional. Even though the names are the same, their meaning can vary depending on the context in which they’re used and, DDD embraces this.
This practice extends to other domain types as well. A class that is a value object in one context might be an entity — or even an aggregate root — in another.
For example, imagine a system in the domain of minting coins. A physical coin or banknote would likely have a unique serial number, making it identifiable even if its value is the same as others. In that context, the coin would not be a value object — it would be an entity, or potentially even an aggregate root if it has internal consistency rules.
This is exactly the kind of reasoning that helps avoid God models — overly generic, overly shared abstractions that blur boundaries and weaken the domain’s expressiveness. By respecting context, we preserve clarity, encapsulation, and business meaning.
Final thoughts
Now that we’ve covered the foundational concepts of Domain-Driven Design, including ubiquitous language, bounded contexts, aggregates, and value objects, we’ve set the ground for more advanced topics.
In the next article, we’ll dive into Domain Events
and Event Sourcing
, exploring how domain behavior can be captured, tracked, and evolved over time using event-driven principles.
Thanks for reading and see you in the next one!
Links worth checking
- Using tactical DDD to design microservices
- Aggregates
- Design a DDD-oriented microservice
- Ubiquitous language
- Bounded context
- Event storming