As systems grow in complexity, traditional development approaches struggle to keep up. Codebases become tangled, business logic gets scattered across layers, and scaling introduces bottlenecks. Domain-Driven Architecture (DDA) offers a structured approach to tackling this complexity, ensuring that software effectively mirrors real-world business problems.
DDA is not suitable for simple applications. However, when building medium to large scale systems or enterprise applications, adopting DDA can make your system scalable, maintainable, and aligned with business goals.
When working with Domain-driven architecture, Your goal isn’t just to build a CRUD system. You’re modeling how the physical business shop works.
Throughout this article, I will use DDA or DDD as the shorthand for Domain-driven architecture/design. To make things a bit easier, I will use Fred and son’s shoe an imaginary e-commerce company as a case study. Fred and son’s shoe is a one stop shop for shoes in Ilora, Nigeria.
This topic is broken down into two articles. In this article, we will explore the core concepts of DDA:
- Domains
- Sub-domains
- Bounded Context
- Entities
- Value Objects
- Aggregates and aggregates roots
- Domain events
- Command Query Responsibility Segregation (CQRS)
Core Concepts of Domain-Driven Architecture
1. Domains
A domain represents the core problem space a business operates in. That’s a fancy way of saying Domain is the core idea your business is built on. The problem you are solving for your customer. For Fred and sons shoe, our domain is shoe sales.
Your domain is the core business logic that rules the software — not the tech stack, not the UI, but the rules, goals, and behaviour of the organisation or problem you're addressing. The most important part of your business that gives you competitive advantage.
What to note when defining your domain
- Your domain must reflects business goals and rules. When defining domain, this is not the time to think about the technical details, the focus should be on what the problems are and how to go about solving them. Technical concerns like tech stack, database choice or infra decisions can wait. The focus is here is core problem definition
- Domains should be defined by business expert, People who understood what the primary focus of the business is. They can be supported by developers to ensure accurate representation and language.
- Try to use common set of terms shared by devs and domain experts as much as possible (e.g., "Order", "Customer", "Customer discount Points"). This will make communication across teams and units easier and processes faster.
- Domain is not just data, but behaviour — e.g., “Apply discount,” “Mark order as fulfilled,” “Calculate tax.”, “Cancel order”
- Domain can evolve. You are not stuck with a definition forever - As the business grows, domain **evolves too — e.g. adding delivery support, new pricing strategies, or international shipping.
A well defined domain will guide developers in building systems that aligns with the business goals. The business should drive the technical requirements not the other way around.
2. Subdomains
A subdomain is a distinct, smaller and focused part of the main domain that manages a specific business function. DDA advocates breaking down domain into subdomains to handle complexity and better align code with business logic.
Each subdomain operates independent yet interconnected, enabling teams to work on specific areas without disrupting the broader system. Subdomain must have a clear and defined boundary - that separates it from other Subdomains.
Using our e-commerce example, here are some subdomains we could come up with:
Product Catalog Subdomain: Handles product listings, categories, attributes (e.g., size, color), and availability.
Example: Add new product, Filter by category, Show out-of-stock itemsOrdering Subdomain: Manages the shopping cart, order creation, and order tracking.
Example: Add to cart, Place an order, Track order statusPayment Subdomain: Handles payment processing, refunds, and invoices.Example: Process credit card, Initiate refund, Send invoice email.
Shipping Subdomain: Deals with delivery options, tracking shipments, and managing logistics. Example: Calculate shipping fee, Update delivery status.
Customer Management Subdomain: Stores customer profiles, preferences, and support interactions.Example: Update address, View order history, Open support ticket
2. Bounded Contexts:
A bounded context defines clear (technical) boundaries within a system, ensuring that each subdomain has its own independent logic, rules, and data models. A model within a specific context behaves consistently across all use cases within the boundary.
This is probably the most task intensive part of DDA. It requires effort and time to map domain to the appropriate context and even more energy and skills to ensure teams respect the boundaries and avoid function overlap (A situation where two or more part of a domain is doing the same thing).
Example:
- The Ordering service tracks items and quantities.
- The Payments service only deals with transactions and invoices.
- They communicate via APIs or message queue services but don’t share databases.
When bounded context is done properly it ensures
- Separation of Concerns: It isolates different parts of a system to reduce complexity and improve maintainability.
- Clarity: Each context uses a consistent language (Ubiquitous Language), avoiding ambiguities in terminology.
- Modularity: Enables independent evolution, scaling, and deployment of components, especially in micro services architecture
- It reduces code sprawl
How is bounded context different from subdomains?
As we get deeper, I would like to take a pause and clarify this before it gets confusing. Many times, bounded contexts and subdomains are misinterpreted and can lead to confusion. Below are the core differences between the two.
Problem space vs Solution Space
A Subdomain exists in the problem space. It represents specific areas of the business domain (e.g., billing, inventory) that need to be taken care of. On the other hand, Bounded Context exists in the solution space, it defines technical boundaries where a consistent domain model is applied to solve problems within a subdomain.Purpose
Sub domain goal is to divides the core domain into smaller manageable parts. While bounded context ensures integrity of domain by defining clear boundaries, preventing ambiguity in terms.Hierarchy:
Subdomains are conceptual and often considered “above” bounded contexts because they represent business concerns. Bounded Contexts are design to implement subdomains in code, making them more technical and solution-oriented
3. Entities and Value Objects
Entities
Entities are part of the core concept in a domain, They are identified by their unique identities and so they are distinguishable from other objects even if their attributes changed over their lifecycle, Their unique identities remain the same.
Few details that can help you understand entities
- Entities are domain objects defined by unique identifier e.g ID, UUID. This identity is persisted regardless of changes to their attributes
- They are persisted over time (e.g., a
User
,Order
, orProduct
). - They are mutable and they change over their life cycle.
- Their identities and continuity of the object are crucial, take tracking the same customer across transaction for example.
- Example is “a customer” entity with a unique customer ID or a customer wallet entity with a unique wallet ID
Value Objects
On the other hand, value objects are immutable. This mean they do not change throughout their lifecycles. They do not have identities, and are defined solely by their attributes. Two value objects with the same attributes are considered equal. Example: Two addresses with the same street name, postcode, and country are considered the same.
Characteristics of value objects
- Value objects are the descriptive part of the domain.
- They are defined by their attributes.
- Because of their lack of identity, they are interchangeable, value objects with the same attributes can be technically identify as one.
- They are immutable - They cannot change once created
- When thinking of value objects, think of currency, or a price of a product, or address of a customer.
How do you decide whether to use an entity or a value object in your domain model?
When you are confused on what to use, ask your self the question below
- Identity:
- Entity - Has a unique identifier
- Value objects - No unique identifier, it is defined by its attributes
- Mutable
- Entity - They are mutable (changed)
- Value objects - Immutable, once created they remain same through their life cycle
- Equality
- Entity - Equality is based on identity
- Value objects - Equality is based on attribute values
4. Aggregates and Aggregate Roots
An aggregate is a cluster of entities and value objects that are related and are treated as a single unit. Aggregate encapsulates all the details of the unit and only expose required functionalities to the rest of the system through the aggregate root.
Aggregate root is the main entry point for interacting with data in an aggregate. Likewise, only aggregate root interacts with external systems. This ensures consistency and enforces business rules across the unit.
Think of an aggregate as a mansion with lots of goodies inside (Entities and value objects). This mansion has a single golden gate (Aggregate root) which is the only entrance. To get something out or interact with the mansion you need to pass through this gate.
Now a bit technical - Using Delivery as an example in Fred’s shoe.
Delivery Manager is an aggregate - entities are: delivery items, customer details, and address, total price, discount code as value objects. You wouldn’t let the outside world change individual Delivery item or price directly. Instead, they go through the aggregate root DeliveryManager.
DeliveryManager.addItem(product, quantity)
DeliveryManager.removeItem(productId)
DeliveryManager.applyDiscount(code)
DeliveryManager.changeDeliveryTime(newDateTime)
This is an important concepts in DDA and it helps to:
- Protects your data from being changed in weird ways.
- Keeps your system easy to reason about.
- Makes your code more reliable in big systems.
- Prevents inconsistent writes and race conditions in high-load environments.
5. Domain Events
What are domain events ?
Event are occurrence that has happened in the past. Like your birthday or in our e-commerce example, a shoe purchase. Think of domain events in the same way. Domain events are events that capture significant business occurrences in a domain that other aggregates/parts of the system might need to react to.
Domain events enable better separation of concerns among different parts of a domain. Instead of tightly coupling services, events enable loose coupling and asynchronous processing.
How are domain events different from traditional message events ?
Domain events are similar to event message style but also different in one way. With event messaging service like: message queue, message broker or even a service bus (link to the previoud article here), messages are always sent asynchronously and communication are available to different process and machine.
They are not bounded by the boundary context of the domain. On the other hand, with domain event, domain events are raise from a domain operation in progress and wants side effects/reaction to it by other process operating within the same domain.
Likewise, in a traditional messaging system, messages can be produced without a consumer or a reaction. Domain events are quite different as you most certainly only emit events because you want a reaction. In some instances, you also want to treat those reactions as a database transaction where either all the operations related to a domain event finish successfully or none of them do.
For example - Imagine a situation at Fred’s shoe store - An order was placed using a gift card and a discount code was used and it emits an event - You want reactions to the event to: Update the stock count, nullify the discount code so it is not re-used and also manage the customer gift card balance among other things.
Lastly, Domain events and their reactions (the actions triggered afterwards that are managed by event handlers) should occur almost immediately, and within the same domain. Domain event can handled synchronously if necessary.
In DDA, use domain events to explicitly implement side effects across one or multiple aggregates within a domain.
6. Command Query Responsibility Segregation (CQRS)
Command Query Responsibility Segregation (CQRS) is a design pattern that segregates write and read actions for a data store into separate data models. This means a single data store would have two model interfaces. One to manager write operations and the other for read operations. It is important to note that CQRS is not limited to DDA. Other software pattern and architecture can leverage CQRS if the project demands it.
Disclaimer: While CQRS is a powerful tool for DDA, it introduces more complexity to your system. It is only beneficial and it is advisable to only use it if your system requires it.
Only use CQRS if your your system meets one or more of the following:
- Your domain is complex, requiring distinct models for handling reads and writes.
- Scalability and performance are critical concerns.
- Your system requires flexibility to evolve over time without disrupting existing functionality.
- You are not building for a simple domain or CRUD-style applications CQRS may introduce unnecessary complexity
- You have separation of development concerns within the same model. With CQRS a team can work on complex business logic in the write model, and another team develops the read model.
- Divergent Data Requirements: In large applications, there are instances where read and write operations have different data requirements. To place an order at Fred’s shoe you need the customerId, productId and the quantity. On the other hand, to read an order, you need the full customerDTO and the productDTO. These two operations demand to different model from the same data store.
Example: A banking system may have **one database for handling transactions (writes). Another optimised database for quickly retrieving account balances (reads).This pattern optimises API performance and reduces database contention.
This is the first installment of the two part series on DDA
In the second part, we will talk about:
- How DDA Improves Scalability and Maintainability
- Examples of Companies using DDA and how they are using it
- Best Practices for Implementing DDA
- When DDA might not be the best choice
- A personal story
Further Reading & Resources
📖 Books:
- Domain-Driven Design by Eric Evans
- Implementing Domain-Driven Design by Vaughn Vernon
🔗 Articles & Guides:
- Domain-Driven Design in Micro-services
- Jan Stenberg. Domain Events and Eventual Consistency
- Event-Driven Architecture & DDA
- DDA in Microservices - Martin Fowler
- Greg Young on CQRS & Event Sourcing
- Greg Young. What is a Domain Event?
- Domain events: Design and implementation in .Net
- Photo by Joakim Nådell on Unsplash