
In the previous post, I presented the project, its motivation, and architecture. Now, it’s time to start checking some implementations.
- Part 1 - EcommerceDDD 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)
- EcommerceDDD++: Streamlining API Client Generation with Kiota and Koalesce
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 who actively works with developers so they can write code using the same terminology and understand the business workflow in depth. It needs intensive collaboration and knowledge exchange. Over time, developers master the intricacies of the domain 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 their domain-specific language through a glossary of domain dialects and terms, and to reach out to them as much as possible before coding. Depending on the level of experience in that field, from an outsider’s perspective, developers may look like domain experts themselves. Ultimately, knowledgeable developers should be able to transfer business concepts to joiners with ease, and that’s a strong indicator of whether 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.
If 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 thoughtful. Things might have to repeat by business design.
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 within which a specific domain model applies, allowing different parts of a system to have distinct, 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 is the heart of the business and the very reason 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.
With the structure above, I could say that the OrderProcessing is the Core domain, since 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 clearly defined boundaries is vital, 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 note that the definitions of domain and subdomain vary depending on the project and its business goals.
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
To implement DDD as the development approach, you must use both Strategic and Tactical design concepts to streamline the design across different aspects and concerns.
The Strategic design focuses on the high-level concepts for the whole solution, such as defining the Bounded contexts, Context mapping, and Subdomains. This will define the overall organization of the entire system.
The Tactical design is concerned with defining the details of the Aggregates, Value Objects, Entities, Repositories, Services, etc.
Although I won’t cover domain mapping technique in this series, different techniques can be used to drive this design, including Event storming, a workshop-based session aiming to bring domain intricacies to light directly from domain experts in collaboration with developers, stakeholders and other roles, similarly 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 simplicity in this project, I only needed a microservice per bounded context, but since the csproj file has a similar name to the bounded context, I felt I needed to clarify the relationship 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, which is a common pattern in many companies.
What are Aggregate, Aggregate Root, and Value Object?
In Domain-Driven Design, an Aggregate is a cluster 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 allowed to interact only with the root, ensuring 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 can also encapsulate logic and validation, making them ideal for modeling types such as IDs, money, quantities, dates, and other values that support the aggregate.
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 architectural 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:
Productis 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
Createmethod 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 post, 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.
Links worth checking
- Using tactical DDD to design microservices
- Aggregates
- Design a DDD-oriented microservice
- Ubiquitous language
- Bounded context