Domain Driven Design
- Pavol Megela
- Sep 22, 2022
- 7 min read
Updated: Mar 9

As applications grow, managing business logic and maintaining scalability become difficult. Domain-Driven Design (DDD) helps solve this by organizing code around business concepts.
We will look at:
Structuring the project using Entities, Aggregates, and Repositories
Implementing a Product & Stock aggregate with domain rules
Using Domain Events to maintain consistency across aggregates
Exposing the logic via NestJS services and controllers
DDD concepts we will talk about:
Entities - Objects with an unique identity
Aggregates - Groups of related entities with strict business rules. Read more about aggregates in Invariants and Aggregates post
Repositories - Abstractions for database access
Domain Events - Events triggered when important domain actions occur
Orchestrating Aggregates - Multiple aggregates grouped together to orchestrate atomic updates
Read Models - Not really a DDD pattern but good to know when learning about DDD principles
Setting Up a DDD Project in NestJS
Before diving into implementation, let’s look at our project. If you want to learn how to structure your NestJS project effectively you can refer to Clean Architecture Application Structure In NestJS Framework.
src/
├── modules/
│ ├── product/
│ │ ├── domain/
│ │ │ ├── product.entity.ts
│ │ ├── application/
│ │ │ ├── product.service.ts
│ │ ├── infrastructure/
│ │ │ ├── product.repository.ts
│ │ ├── product.controller.ts
│ │
│ ├── stock/
│ │ ├── domain/
│ │ │ ├── stock.entity.ts
│ │ │ ├── events/
│ │ │ │ ├── stock-updated.event.ts
│ │ ├── application/
│ │ │ ├── stock.service.ts
│ │ ├── infrastructure/
│ │ │ ├── stock.repository.ts
│ │ ├── stock.controller.ts
Now, let’s implement a Product and Stock aggregate.
Entity
An entity is an object that has a unique identifier and exists independently. Even if its properties change it will remain the same entity. For example, in an e-commerce system, a Product is an entity because:
It has an ID ("product-123"), which uniquely identifies it
Its name, price or description can change, but it’s still the same product
Key takeaways for creating entities
Always give entities a unique ID usually from the database
Business logic should be inside the entity, not in the service layer
Use methods like updatePrice() to enforce rules within the entity
Invariants & Aggregate
If you want a deeper dive into aggregates and invariants and understand how they are created and grouped together, check out this article Invariants and Aggregates.
Aggregate is a group of related entities that must be kept consistent according to business rules (invariants). The aggregate root is the main object responsible for enforcing these invariants.
For example, in an Order aggregate, you can’t add or remove items after checkout because that would break the business rule. In a Stock aggregate, inventory should never be below zero, because its impossible to have negative amount of items in stock. Aggregates help enforce these rules without requiring every part of the system to check them manually.
Product
The Product entity is our Aggregate Root (modules/product in the folder structure showed above). It contains business logic and enforces invariants.
Let's create Product and make sure that our business rules are enforced:
Product price cannot be negative or equal to 0
// src/modules/product/domain/product.entity.ts
export class Product {
public constructor(
public readonly id: string,
public name: string,
public price: number,
public category: string,
public description: string
) {
this.validatePrice(this.price);
}
public updatePrice(newPrice: number): void {
this.validatePrice(newPrice);
this.price = newPrice;
}
public validatePrice(newPrice: number): void {
if (newPrice <= 0) {
throw new Error('Price must be valid.');
}
}
}
Here, Product ensures that a valid price is always enforced, in both cases, when a new Product is created and when the price is updated. This is done by calling validatePrice() in both the constructor and the updatePrice() method. If an invalid price (zero or negative) is provided, an error is thrown, preventing the system from entering an invalid state. This way, the business rule is always enforced at the domain level, ensuring consistency across the application.
Stock
Since Stock has a different lifecycle and doesn’t need atomic updates with Product, it should be a separate aggregate. (read about how we got to this decision here Invariants and Aggregates).
Let's look create Stock and make sure that our business rules are enforced:
Stock cannot be negative
// src/modules/product/domain/stock.entity.ts
export class Stock {
public constructor(
public readonly id: string,
public readonly productId: string,
public readonly warehouseId: string,
private quantity: number
) {
this.validateQuantity(quantity);
}
public decreaseStock(amount: number) {
const newQuantity = this.quantity - amount;
this.validateQuantity(newQuantity);
this.quantity = newQuantity;
}
public getQuantity() {
return this.quantity;
}
public validateQuantity(newQuantity: number): void {
if (newQuantity < 0) {
throw new Error("Stock cannot be negative.");
}
}
}
Stock ensures that quantity can never be negative, again in both cases, when a new Stock instance is created and when stock is decreased. The validateQuantity() method is called in the constructor to enforce this rule at creation and again inside decreaseStock() before applying the change. If an invalid quantity is detected, an error is thrown, preventing the system from processing an invalid operation. This guarantees that stock levels are always valid and that no part of the system can reduce stock below zero.
Repositories
A repository is a class that provides methods for retrieving and saving aggregates, without exposing database logic to the domain layer.
Instead of querying the database directly inside your service, you call a repository method like orderRepository.findById(orderId).
Key takeaways for creating repositories
Repositories fetch and save entire aggregates, not just entities
Keep repositories separate from business logic, they should only handle data persistence
Your domain layer (entities) should never interact with the database directly, only through repositories
I did mention Domain layer, if you want to learn about Layers in Clean Architecture, what I really recommend, you can quickly read this short article about them Clean Architecture Layers.
Domain Events
A domain event is a message that is triggered when an important change happens in the system. Instead of directly modifying another aggregate an event is published, and any part of the system that needs to react can listen for that event.
For example, when stock is decreased for a product, the system might need to:
Notify the warehouse to reorder stock
Update the analytics service
Log the change
Instead of Stock calling multiple services directly it emits an event and other parts of the system handle it asynchronously.
Key takeaways for creating domain events:
Domain events decouple aggregates, making the system more flexible, scalable and easier to change
Events are stored inside the entity first, then published by the application service
Handlers react to events asynchronously, so multiple systems can respond without tight coupling
You can see simple example here Handling Domain Events in NestJS Server.
Domain Events Complexity
While domain events are a powerful way to decouple parts of your system you have to keep in mind that they also introduce complexity. Managing event consistency, ensuring proper event handling and dealing with eventual consistency are challenges you’ll need to consider.
Orchestrating Aggregate
In simpler applications where keeping complexity low is a priority you might not want to implement Domain Events. Instead, you can take a more straightforward approach by introducing an Orchestrating Aggregate. This is a single aggregate that encapsulates and manages operations across multiple aggregates.
This approach allows a higher-level aggregate to own and coordinate business rules between related aggregates while maintaining strong consistency. It works well in cases where aggregates frequently interact and need to be updated together in a single transactional operation.
When to use a Orchestrating Aggregate instead of domain events
When two aggregates must be updated together in a single transaction
When introducing eventual consistency (via domain events) would complicate business logic or increase application complexity more than is necessary
When there is a clear business concept that justifies combining the two
Example: Order and stock should be updated together
If we need to ensure Stock is decreased immediately when an Order is placed, we can introduce an OrderProcessing aggregate that owns the logic of both Order and Stock. That means that instead of having Order modify Stock directly or relying on Domain Events, this aggregate would:
Import both the Stock and Order modules
Access their services directly (OrderService and StockService)
Perform operations on both aggregates within a single method call, possibly updated together as a single transaction (atomicity) utilizing Unit-Of-Work Pattern
Ensure business rules are applied correctly before persisting changes.
Read Models
While this is not a formal DDD pattern, it aligns well with DDD principles. I want to mention it here so you have an idea that this is possible to do.
Read Model is a read-only model that combines data from multiple aggregates into a single structure optimized for queries. Instead of fetching and joining multiple aggregates at runtime, you:
Create a SQL VIEW that merges relevant fields from different tables
Precompute and store a model (Materialized View) in a separate read database (Elasticsearch, Redis, a reporting database)
Query the View Aggregate instead of querying multiple aggregates separately to improve performance
When to Use View Aggregates:
When read performance is more important than write performance
When you want to combine data from multiple aggregates without breaking aggregate boundaries
When reporting or analytics require pre-joined data for fast queries
Keep reading
For cases where aggregates need to communicate asynchronously, Domain Events are a powerful solution. To see a simple implementation of Domain Events in NestJS, check out this article.
And that's all for now
We explored key Domain-Driven Design (DDD) concepts like Entities, Aggregates, Repositories, and Orchestrating Aggregates. We also discussed different approaches to handling operations across multiple aggregates, including using an Orchestrating Aggregate or Read Models for optimized queries.
By applying these DDD principles correctly, you can build scalable, maintainable, and well-structured applications. Keep on learning, you're doing great!
Commenti