Clean Architecture, Domain Driven Design

Table of Contents

1.     What is a Domain?

o   Understanding Abstraction

o   Building a Domain Model

o   Example: E-Commerce Domain

2.     The Strategic Phase of Domain-Driven Design (DDD)

o   Purpose of the Strategic Phase

o   Key Concepts

o   Example Dialogue: Aligning on a Ubiquitous Language

o   Subdomains

o   Why is the Strategic Phase Important?

3.     The Tactical Phase of Domain-Driven Design (DDD)

o   What is Tactical DDD?

o   What Tactical DDD Covers (Scope)

o   Tactical DDD Phases

o   Minimal Example – E-commerce Order Context

o   Programming Paradigms in Tactical DDD

4.     The DDD Four Conceptual Layers

5.     Data Transfer Objects (DTOs)

o   Why Use DTOs?

o   What Are DTOs?

o   Why DTOs Are Important

o   Trade-offs

o   Example

o   The Responsibility of Each Layer in Generating DTOs

6.     Modules

7.     Value Objects

8.     Aggregates

9.     Factories

10.  Repositories

11.  Domain Services

12.  Application Layer

·       Delegation from Presentation Model

·       Domain Payload Objects (DPO)

·       State Representations in REST

·       Use Case Optimal Repository Queries

·       Supporting Multiple Clients

·       Role of Application Services

·       Command Objects

·       Composing Multiple Bounded Contexts

13.  Aggregates and Event Sourcing

14.  REST and DDD – Why Not Expose Aggregates Directly?

What is a Domain?

A domain is simply a part of the real world that we want to represent in software. You cannot directly “pour” reality into code; instead, you must build an abstraction—a structured way to represent real-world knowledge in terms that software can understand.

In Domain-Driven Design (DDD), the domain is central:

  • It defines the core concepts, rules, and processes of the business.

  • It gives us the language (Ubiquitous Language) to reason about the system consistently across developers, domain experts, and stakeholders.

  • It drives the structure of both the model and the code.


Why Does It Matter?

If we misunderstand the domain, the software may work technically but fail to solve the real problem. DDD emphasizes modeling the domain accurately so the software reflects the business truth.


1. Understanding Abstraction

In Manufacturing and Engineering

Abstraction means simplifying a complex system by focusing only on its essential features while ignoring unnecessary details. This helps engineers and designers manage complexity, improve communication, and design systems more efficiently.

Examples:

  • CAD Modeling: A 3D model of a machine may hide internal parts and only show the dimensions necessary for assembly.

  • System Design: Mechanical, electrical, and software layers are designed separately before being integrated.

  • Simulation Models: A factory process can be simulated without modeling every detail, just enough to test efficiency.

Why it matters:

  • Makes complex systems easier to manage.

  • Improves clarity and problem-solving.

  • Encourages modular design (e.g., interchangeable or reusable parts).


Example: E-Commerce Domain
Consider an e-commerce system:
  • The domain includes concepts like Product, Order, Customer, and Payment.
  • The rules include things like “An order cannot be shipped before it’s paid.”
  • The processes cover scenarios such as order placement, cancellation, and fulfillment.
All of these form the domain knowledge that the software must capture.


In Software Architecture

In software, abstraction means organizing the system around high-level components and their interactions, while hiding internal implementation details.

This allows us to:

  • Create logical layers (UI, application, domain, infrastructure).

  • Build modules with clear purposes.

  • Define services with stable interfaces.

Eric Evans’ view:
A domain model is not just a diagram. It is an organized representation of domain knowledge—something that can be expressed in diagrams, carefully written code, or even plain language.


2. Building a Domain Model

We learn about the domain by talking to domain experts and collecting raw knowledge. But raw knowledge alone cannot be transformed into software. To make it useful, we must build a mental blueprint—an abstraction of the domain.

  • At first, this blueprint is incomplete.

  • Over time, by refining our understanding, testing assumptions, and collaborating with experts, the model becomes clearer, richer, and more accurate.


3. Example: E-Commerce Domain

Imagine designing an e-commerce system. The domain includes:

  • Entities: Customer, Order.

  • Value Objects: Address, Money.

  • Aggregates: OrderAggregate (manages line items, payments, shipping, and status).

Here, you cannot directly code "shopping behavior," but by abstracting it into entities, value objects, and aggregates, you create a software model that represents real-world shopping processes.

Final Note:
The domain is the foundation. Everything in DDD — strategic and tactical — builds on a clear, shared understanding of the domain. Without it, architecture becomes disconnected from business reality.


The Strategic Phase of Domain-Driven Design (DDD)

Domain-Driven Design (DDD) emphasizes that large systems should not rely on one unified model for everything. Instead, it introduces the concept of Bounded Contexts—separate models tailored for specific areas of the system. Each context reflects the unique language and rules of its subdomain.

Purpose of the Strategic Phase

The strategic phase of DDD focuses on high-level domain understanding. It helps us:

  • Identify the core domain — the most critical part of the business where differentiation and innovation matter most.

  • Define bounded contexts — each with its own domain model, ensuring clarity and preventing conflicts.

  • Map relationships between contexts — such as partnerships, dependencies, or integrations.

  • Improve communication — by aligning technical and business teams through a shared Ubiquitous Language.


Example:

Imagine building a project management platform. Instead of one massive model, you create bounded contexts:

  • Task Management Context (tasks, deadlines, assignments)

  • Collaboration Context (forums, discussions, messages)

  • Billing Context (subscriptions, invoices, payments)


Each context has its own model and language, but together they form a cohesive system. The strategic phase defines these boundaries and ensures smooth interaction between them.


Why It Matters

  • Clarity: Each team works with its own consistent model.

  • Scalability: Different contexts can evolve independently.

  • Integration: Boundaries are explicit, making communication between systems clear.


Key Concepts

1. Core Domain

The most important part of the business that provides a competitive advantage.

Is a part of the business Domain that is of primary importance to the success of the organization.

Example: In an e-commerce system, the pricing and discount engine might be the core domain.

2. Bounded Contexts

defines the limits within which a specific domain model is consistently understood (specific business domain terms), ensuring clarity (each term has a specific meaning in this BC), consistency, and separation from other models in different contexts.

Example:

In an e-commerce system, the term “Order” might mean something different to the Shipping team than it does to the Inventory team. Each of these would have its own bounded context with its own model of what an “Order” is.



3. Context Mapping

Defines how different bounded contexts interact with each other.

In Domain-Driven Design (DDD), context mapping is a way to visualize how different parts (bounded contexts) of your system interact with each other and with external systems.
A Bounded Context is a clearly defined area of your application where a specific domain model applies, with its own rules, language, and data structures.

A context map helps you answer questions like:

  • Where are the boundaries? (Which teams or services own which parts of the system?)

  • How do they communicate? (Direct calls, APIs, messaging, events?)

  • What are the relationships? (Do they depend on each other? Who leads and who follows?)

  • What translations are needed? (Do you have to convert data formats, rename fields, or change units?)

When drawing your context map:

  1. Identify each bounded context in your project (core domains, supporting domains, external systems).

  2. Draw them as separate boxes with clear boundaries.

  3. Show relationships with arrows or lines indicating the flow of communication or dependencies.

  4. Label integration types — for example:

    • REST API

    • Message Queue / Event Bus

    • Database Replication

  5. Mark translation requirements (e.g., “Convert USD to SAR” or “Map internal user ID to external system ID”).

  6. Include teams — note which team owns or maintains each bounded context.

💡 Remember: this diagram is not just for documentation. Your code, integrations, and workflows should reflect what’s in the diagram.


Legend:

  • Boxes = bounded contexts

  • Lines/arrows = integration direction

  • Labels = type of integration (API, Event, Import, etc.)

  • Notes = translation/formatting rules

4. Ubiquitous Language    

   A shared, well-defined language between developers, domain experts, and stakeholders.

Example: Using "Order Confirmation" instead of vague terms like "Processed Order."

Example Dialogue: Aligning on a Ubiquitous Language

BA: Let’s start by going through the process of handling orders. Could you walk me through it?

C: Sure. A User browses the menu, puts food into their Basket, and then confirms the Purchase.

BA: Okay — you said “User,” “Basket,” and “Purchase.” Earlier you mentioned “Customer” and “Cart” in our email. Should we agree to consistently call the person placing an order a Customer and the temporary food collection a Cart?

C: Yes, “Customer” and “Cart” are fine.


BA: Great. Once the order is placed, what happens next?

C: The request goes to the Vendor, and they can either Approve or Decline it.

BA: Just to clarify — when you say “Vendor,” is that the same as the Restaurant?

C: Yes, I mean the restaurant.

BA: Let’s stick with “Restaurant” everywhere so we avoid confusion. And instead of mixing “Approve” and “Accept,” which one feels more natural to you?

C: Let’s go with “Accept.”


BA: Perfect. What happens after the restaurant accepts?

C: We charge the Client’s credit card.

BA: The “Client” here is still the Customer, correct?

C: Yes, the same person.

BA: Then we’ll use “Customer” consistently instead of “Client.”


BA: After payment, what’s next?

C: The kitchen starts Cooking the order, then the Courier picks it up.

BA: Is “Cooking” the same as “Preparing,” and is “Courier” the same as “Delivery Driver”?

C: Yes to both.

BA: Okay — let’s use “Preparing” for the kitchen stage and “Delivery Driver” for the person delivering.


BA: How does delivery tracking work?

C: The driver marks it as On Route and then Completed.

BA: To keep it consistent with earlier terms, can we use “On the Way” instead of “On Route” and “Delivered” instead of “Completed”?

C: Yes, that works.

Final Agreed Ubiquitous Language

TermDefinition
CustomerPerson placing an order
CartTemporary collection of items before placing an order
OrderA confirmed request for food items
RestaurantThe food provider receiving the order
Accept / RejectRestaurant’s decision on whether to fulfill the order
PaymentProcess of transferring money from customer to restaurant
Credit Card / Digital WalletSupported payment methods
PaidStatus after successful payment
PreparingRestaurant is making the food
Delivery DriverPerson delivering the order
On the WayDriver is transporting the order to the customer
DeliveredOrder has reached the customer

Building a language like that has a clear outcome: the model and the language are strongly interconnected with one another. A change in the language should become a change to the model.

5. Subdomains

Subdomain is one specific area of the whole business domain(i.e.: Product Catalog, Orders, Invoicing, and Shipping).

The domain is broken into three subdomains:

Core Domain – The most valuable part (e.g., recommendation engine for Netflix).

Supporting Subdomain – Necessary but not the main focus (e.g., user management).

Generic Subdomain – Standard functionalities that can be outsourced (e.g., payment processing)



Why is the Strategic Phase Important?

Ensures clear separation of concerns between different parts of the system.

Prevents miscommunication by enforcing a shared ubiquitous language. Helps design a scalable and maintainable architecture.

Enables better team collaboration by defining ownership of bounded contexts.

The Tactical Phase of Domain-Driven Design (DDD)

1. What is Tactical DDD?

Tactical DDD is the hands-on part of Domain-Driven Design. While strategic DDD maps the big picture (contexts, team boundaries, and integrations), tactical DDD works inside a single bounded context and defines the building blocks that turn business language and rules into working code.

It is achieved through a progression of steps: starting with lightweight modeling (e.g., UML), moving to partial coded abstractions, and finally producing executable code. At the end, the final code becomes the true domain model—it is where business rules are enforced.


2. What Tactical DDD Covers (Scope)

Inside one bounded context, tactical DDD specifies:

  • Entities – objects with identity (e.g., Order, Customer).

  • Value Objects – immutable, identity-less concepts (e.g., Money, Address).

  • Aggregates – small clusters of Entities/Value Objects with one Aggregate Root that enforces rules (e.g., Order as root of OrderLines).

  • Domain Services – stateless domain operations that don’t fit neatly in a single entity (e.g., TaxCalculator).

  • Domain Events – facts that happened (e.g., OrderPaid, OrderShipped) used to coordinate across aggregates/contexts.

  • Repositories – collection-like access to Aggregates (e.g., OrderRepository).

  • Factories – controlled creation for complex objects/aggregates.

The goal is to:

  • Make business rules explicit and enforced.

  • Keep consistency inside an Aggregate and coordinate across Aggregates with events.

  • Produce a model that’s expressive, testable, safe to evolve, and aligned with the business language.


3. Tactical DDD Phases

Tactical DDD often unfolds in three practical phases:

Phase 1 – UML / Lightweight Modeling

  • Purpose: Capture the first abstraction of the domain.

  • Method: Use UML diagrams, sketches, or whiteboards.

  • Example:
    A diagram showing OrderOrderLine with a rule “total ≥ 0”.

  • Role: Communicate structure and rules with domain experts and developers.

  • Note: UML here is only for understanding and sharing ideas, not the final model.


Phase 2 – Partial Code Abstractions

  • Purpose: Translate the model into early code structures.

  • Method: Write stubs, interfaces, and empty classes.

  • Example (C# stub):

    csharp
    public class Order {
    public Guid OrderId { get; }
    private List<OrderLine> _lines = new();
    // TODO: Add rules for totals and status transitions
    }
  • Role: Validate naming against ubiquitous language, see how the abstractions feel in code, and prepare for rules.


Phase 3 – Final Coding (Executable Domain Model)

  • Purpose: Implement rules, invariants, behaviors, and persistence.

  • Example (C# implementation):

    csharp
    public class Order {
    public Guid OrderId { get; }
    private readonly List<OrderLine> _lines = new();
    public Money Total => _lines.Sum(l => l.Subtotal);
    public void Pay(PaymentReference paymentRef) {
    if (Total.IsZero) throw new InvalidOperationException("Order total must be positive.");
    // emit OrderPaid event
    }
    }
  • Role: The code becomes the true domain model. UML and stubs may remain as documentation, but only the code enforces the business rules.

 

4. Minimal Example – E-commerce Order Context

Bounded Context: Order Management

This example shows the same model across the three tactical phases:
(1) UML / lightweight modeling → (2) partial code abstractions → (3) final executable code.

Phase 1 — UML / Lightweight Modeling (communication & shape)

Use a small UML sketch to agree on names, relationships, and invariants. Keep it focused—this is for shared understanding, not for code generation.

At this stage, the team sketches the building blocks to communicate with domain experts and align on language.

  • Entity (Aggregate Root): Order(OrderId)/Order as the root containing multiple OrderLines

  • Value Objects: Money, Address, OrderLine

  • Aggregate: Order as the root containing multiple OrderLines

  • Rules (invariants):

    • total >= 0

    • cannot pay if status == Cancelled

    • cannot ship unless status == Paid

  • Domain Services (externalized rules): TaxCalculatorService (regional rules)

  • Repositories (persistence placeholder): OrderRepository

  • Factories (creation logic): OrderFactory.createFromCart(cart, customer)

  • Events (facts): OrderPlaced, OrderPaid, OrderShipped

What UML Does in This Context (final, aligned with the diagram)

  • Names core elements: Order (Aggregate Root), OrderLine, Money, Address, and supporting IDs (OrderId, ProductId, PaymentRef, TrackingNumber).

  • Defines boundaries: Shows Order as the Aggregate Root that composes OrderLine(1..*), references Address, and encapsulates the consistency boundary.

  • Documents invariants (as notes):

    • total() >= Money.zero

    • cannot pay if status == Cancelled

    • cannot ship unless status == Paid

  • Flags domain events emitted by the Aggregate: OrderPlaced, OrderPaid, OrderShipped (and OrderCancelled if included).

  • Positions supporting patterns (externals to the Aggregate):

    • TaxCalculatorService (Domain Service) used by Order

    • OrderRepository (Repository) persisting Order

    • OrderFactory (Factory) creating Order (e.g., createFromCart)

  • Keeps scope tight: One Aggregate + nearby Value Objects; detailed publishing mechanisms, persistence specifics, or cross-context flows are kept in text, not the class diagram.



Phase 2 — Partial Code Abstractions (shaping the API surface)

Now the abstractions are expressed in code, showing what operations exist but not their internals.

Entity & Aggregate Root (Order):

csharp
class Order { OrderId Id; List<OrderLine> Lines; Address ShippingAddress; Money Total; OrderStatus Status; void place(List<OrderLine> lines, Address address); void pay(PaymentRef paymentRef); void ship(TrackingNumber trackingNumber); void cancel(string reason); }

Value Objects:

csharp
record Money(decimal Amount, string Currency);
record Address(string Street, string City, string Country); record OrderLine(ProductId product, int quantity, Money price);

Services:

csharp
interface TaxCalculatorService {
Money calculateTax(Order order); }

Repositories:

csharp
interface OrderRepository {
Order findById(OrderId id); void save(Order order); }

Factory:

csharp
class OrderFactory {
static Order createFromCart(Cart cart, Customer customer); }

Events:

csharp
record OrderPlaced(OrderId Id, DateTime At);
record OrderPaid(OrderId Id, PaymentRef Ref); record OrderShipped(OrderId Id, TrackingNumber Tracking);
👉At this stage, we’ve outlined the API, but the business rules aren’t fully applied yet.”

Phase 3 — Final Coding (enforcing invariants & emitting events)

Here, invariants, business rules, and event emission are implemented.

Entity / Aggregate with Rules:

csharp
class Order {
void place(List<OrderLine> lines, Address address) { if (lines.Count == 0) throw new InvalidOperationException("Order must have items"); this.Lines = lines; this.ShippingAddress = address; this.Status = OrderStatus.Placed; emit(new OrderPlaced(this.Id, DateTime.Now)); } void pay(PaymentRef paymentRef) { if (Status != OrderStatus.Placed || Total.Amount <= 0) throw new InvalidOperationException("Order cannot be paid in this state"); this.Status = OrderStatus.Paid; emit(new OrderPaid(this.Id, paymentRef)); } void ship(TrackingNumber trackingNumber) { if (Status != OrderStatus.Paid) throw new InvalidOperationException("Cannot ship unpaid order"); this.Status = OrderStatus.Shipped; emit(new OrderShipped(this.Id, trackingNumber)); } }

Event-Driven Collaboration Example:

  • OrderPaid → triggers Billing microservice to capture funds.

  • OrderShipped → triggers Shipping microservice to dispatch items.

At this stage, the abstractions become working code, with enforced invariants and events integrated into the system.

Programming Paradigms in Tactical DDD

When tactical patterns (Entities, Value Objects, Aggregates, etc.) move from modeling to code, the programming paradigm matters. Not all paradigms support model-driven design equally well.

Object-Oriented Programming (OOP) – the natural fit

OOP fits naturally with domain modeling because both emphasize objects, identity, and relationships.

  • Classes ↔ Entities/Value Objects: You can model an Order class as an Aggregate Root, or a Money class as a Value Object.
  • Associations ↔ Relationships: Order contains OrderLine as part of its Aggregate.
  • Messaging ↔ Domain Events: Objects communicate by sending messages, just like publishing domain events.

This makes OOP languages (Java, C#, Kotlin, etc.) are particularly well-suited for implementing tactical DDD. They provide a natural bridge between the conceptual model and the final code, making the translation from model → code direct and expressive.

Procedural Programming (PP) – when it falls short

Procedural programming focuses on algorithms and step-by-step procedures. While it’s effective in domains like mathematics or scientific computing (where the problem is mostly numerical calculations), it doesn’t map well to complex, evolving domains like e-commerce or banking.

For example:

  • A mathematics library for calculating prime numbers or matrix multiplications is naturally procedural.
  • But modeling a Customer placing an Order requires identity, state, rules, and interactions — things procedural programming struggles to capture directly.

Therefore, PP can complement tactical DDD for calculation-heavy modules, but OOP is generally the better foundation for implementing rich domain models.

 


Summary:

  • Phase 1 (UML): Communicates the shape (entities, values, aggregates, services, repos, factories, events).

  • Phase 2 (Partial Code): Defines API surfaces and abstractions.

  • Phase 3 (Final Code): Enforces invariants, raises domain events, and integrates with other contexts.

This phased approach shows how Tactical DDD evolves from sketches, to abstractions, to robust, executable code.


The DDD four conceptual layers

In many applications, most code isn’t about the core business logic—it’s about databases, files, networks, or the user interface. If this “infrastructure” code gets mixed with the business logic, the system becomes hard to understand, design, and automatically test. Even small UI changes can accidentally change business rules, and changing a rule might require editing code in multiple unrelated places.

The solution is to separate the program into layers. Keep domain logic in its own layer, apart from UI, application services, and infrastructure. This separation keeps the domain model focused, easier to improve, and able to clearly express business knowledge. A typical domain-driven design uses four conceptual layers:

1. User Interface (UI) Layer

  • Responsibility: Handles input/output with the user or external systems (e.g., Web UI, REST API, CLI).

  • Key point: Should not contain business logic. It simply coordinates with the application layer.

  • Example: A React front-end displaying orders, or a REST API controller forwarding a request to the application layer.


2. Application Layer

  • Responsibility: Orchestrates tasks and use cases by coordinating domain objects.

  • Key point: Contains workflows but no business rules.

  • Example: An OrderService class that calls the domain Order aggregate to validate and process an order.


3. Domain Layer

  • Responsibility: The heart of the system, containing business rules, domain models, and invariants.

  • Includes:

    • Entities: Objects with identity (e.g., Order, Customer).

    • Value Objects: Immutable objects defined by their attributes (e.g., Address, Money).

    • Aggregates: Clusters of entities/values with a single root (e.g., OrderAggregate controlling OrderLines).

    • Domain Services: Stateless operations that don’t naturally belong inside a single entity (e.g., CurrencyConversionService).

    • Domain Events: Capturing facts that happened in the domain (e.g., OrderPaid).

    • Factories: Create complex aggregates consistently.

    • Repositories (Interfaces): Abstract persistence concerns (IOrderRepository).

⚠️ Important: Only interfaces for repositories live here, not the persistence code itself.


4. Infrastructure Layer

  • Responsibility: Provides technical capabilities (databases, messaging, file systems, external services).

  • Implements:

    • Repository implementations (SqlOrderRepository).

    • Domain service implementations that depend on external systems (e.g., calling a payment gateway).

    • Technical concerns (logging, email sending).


 

Example: Interaction Between Application, Domain, and Infrastructure Layers

Imagine a user wants to book a flight route. The process might work like this:

  1. User Request (UI Layer → Application Layer)

    • The user enters the desired route, dates, and passenger details in the booking interface.

    • The UI sends this information to an application service in the application layer, such as FlightBookingService.

  2. Application Layer Orchestration

    • The FlightBookingService receives the request and coordinates the booking process.

    • It retrieves the necessary domain objects (e.g., Flight, Route, Passenger) from the infrastructure layer via repositories like FlightRepository or RouteRepository.

  3. Domain Logic Execution (Domain Layer)

    • The application service invokes methods on these domain objects.

    • For example, the Flight object might run CheckSecurityMargins() to ensure there is enough separation from other already booked flights.

    • The domain layer enforces business rules such as seat availability, flight scheduling constraints, and passenger eligibility.

  4. Decision and State Update (Domain Layer)

    • If all checks pass, the domain objects update their internal state to reflect that the booking is “decided” or “confirmed”.

    • These changes happen entirely within the domain layer, keeping business logic isolated from infrastructure concerns.

  5. Persistence (Application Layer → Infrastructure Layer)

    • After the domain objects are updated, the application service calls repository methods to save the new booking state back to the infrastructure layer.

    • The infrastructure layer handles the actual persistence, such as storing the updated objects in a database.

Dependency Inversion (Tiny Interface vs. Implementation)

A key principle in DDD layering is that the domain layer should never depend on infrastructure. Instead:

  • The domain defines tiny interfaces (e.g., IPaymentGateway, IOrderRepository).

  • The infrastructure layer provides implementations (StripePaymentGateway, SqlOrderRepository).

  • This keeps the domain pure and testable.

When a Domain Service uses multiple Aggregates

Sometimes a business rule spans more than one Aggregate and doesn’t fit neatly inside either Aggregate’s root. That’s a classic case for a Domain Service.

Key rules:

  • The service can read from multiple Aggregates in one use case.

  • As a rule of thumb, modify only one Aggregate per transaction; coordinate others by Domain Events (eventual consistency) if needed.

  • Prefer Application to load aggregates and pass them to the Domain Service. That keeps the domain implementation pure and testable.

Where do the interface and implementation live?

  • Interface: always in the Domain layer (it’s part of the Ubiquitous Language).

  • Implementation:

    • If it’s pure domain logic (no external calls), the implementation can live in the Domain layer.

    • If it needs external data (e.g., FX rates, tax API), keep the Domain Service implementation in Domain, but depend on other Domain-defined interfaces (e.g., IExchangeRateProvider) whose implementations live in Infrastructure.

    • Alternatively, if you prefer maximum purity, you can keep only the interface in Domain and put the implementation in Application when it’s orchestration-flavored. (But then it’s arguably an Application Service, not a pure Domain Service.)


This structured approach shows clearly how each layer has its own responsibility:

  • The UI layer handles input/output with the user.

  • The application layer coordinates actions and workflow.

  • The domain layer enforces business rules and state changes.

  • The infrastructure layer manages technical concerns like data storage.

Entities

Entities represent domain objects that are defined primarily by their identity rather than by their attributes. Their attributes can change over time, but the entity remains the same as long as its identity is preserved.

  • Definition: An entity is unique because of its identity, not its data.

  • Example: Two customers may both be named John Smith and be 35 years old, but they are distinct entities if they have different CustomerId values.

  • Why It Matters: Entities allow us to track and manage things that evolve over time (like Customers, Orders, or Products), ensuring that changes to their attributes don’t create “new” objects accidentally.

Code Illustration (C#)

public class Customer
{
   public Guid CustomerId { get; private set; }  // Identity
   public string Name { get; set; }
   public int Age { get; set; }
   public Customer(Guid id, string name, int age)
   {
       CustomerId = id;
       Name = name;
       Age = age;
   }
   // Equality based on identity
   public override bool Equals(object obj)
       => obj is Customer other && CustomerId == other.CustomerId;
   public override int GetHashCode() => CustomerId.GetHashCode();
}



Value Objects

Value Objects represent descriptive aspects of a domain without identity. They focus on attributes rather than being unique objects. For example, an address (street, city, state) can be a Value Object linked to a customer. Value Objects should be:

  • Simple and small

  • Immutable if shared

  • Easy to copy or discard without side effects

They can contain other Value Objects and sometimes references to Entities. Attributes that logically belong together form a Value Object, rather than being scattered.


Domain Services

Sometimes, domain behavior (actions) doesn’t naturally belong to any single Agrrigate, Entity or Value Object. For example, transferring money between accounts doesn’t fit well inside either account object. In such cases, Services are used.

Characteristics of Services:

1.    Represent domain operations that don’t fit naturally into Entities or Value Objects.

2.    Work with multiple domain objects.

3.    Are stateless (no internal state).

Services group related behaviors clearly and avoid cluttering Entities or Value Objects with unrelated responsibilities.

What a Domain Service Is Not

·       A Domain Service is not the same as a service in Service-Oriented Architecture (SOA).

o   SOA services are usually coarse-grained, remote, system-level APIs, often built with RPC or messaging technologies for integration between distributed systems.

o   Domain Services, on the other hand, live inside the domain model and are focused on business logic, not on system integration.

·       A Domain Service is also not an Application Service.

o   Application Services orchestrate use cases, coordinate transactions, and act as clients to the domain model.

o   Domain Services encapsulate business logic that doesn’t naturally belong to a single Aggregate/Entity.

o   In practice, Application Services often call Domain Services.

·       The word service in “Domain Service” does not imply that it must be a heavyweight, distributed, or remote operation. It is simply a stateless business operation defined in the domain.


What a Domain Service Is

Domain Service is a stateless operation that belongs to the domain model, expressed in the Ubiquitous Language, and represents a business concept that:

·       Does not naturally fit into a single Aggregate/Entity/Value Object.

·       Often involves multiple Aggregates.

·       Encapsulates domain rules, processes, or transformations.

Typical Uses of Domain Services:

1.    Perform a significant business process

o   Example: TransferMoneyService handling funds movement between two Account Aggregates.

2.    Transform domain objects

o   Example: SchedulingService converting a draft MeetingRequest into a confirmed CalendarEntry.

3.    Calculate complex values that require multiple inputs

o   Example: BurndownChartCalculator computing metrics across multiple Sprint Aggregates in Agile project management.


Roles & Placement (Dependency Inversion)

  • Domain Layer

    • Domain Service interface and (if pure logic) implementation

    • Aggregate types (Account, TransferPolicy)

    • Repository interfaces (IAccountRepository)

  • Application Layer

    • Orchestrates the transaction (begin/commit/rollback)

    • Loads Aggregates via repositories and calls the Domain Service

  • Infrastructure Layer

    • Repository implementations (ORM/SQL)

    • Unit of Work / Transaction plumbing

This keeps the Domain pure and the transaction mechanics outside the model.


Example: Atomic Funds Transfer (debit A, credit B) in one transaction

Domain Layer

Aggregates

public sealed class Account { public Guid Id { get; } public Money Balance { get; private set; } public int Version { get; private set; } // for optimistic concurrency public Account(Guid id, Money opening, int version = 0) { Id = id; Balance = opening; Version = version; } public void Debit(Money amount) { if (amount.Value <= 0) throw new DomainException("Amount must be positive."); if (Balance.Value < amount.Value) throw new DomainException("Insufficient funds."); Balance = new Money(Balance.Value - amount.Value, Balance.Currency); Version++; } public void Credit(Money amount) { if (amount.Value <= 0) throw new DomainException("Amount must be positive."); Balance = new Money(Balance.Value + amount.Value, Balance.Currency); Version++; } } public readonly record struct Money(decimal Value, string Currency);

Repository interface & Domain Service interface

public interface IAccountRepository { Account FindById(Guid id); void Save(Account account); // honors optimistic concurrency on Version } public interface ITransferService { void Transfer(Account from, Account to, Money amount); }

Domain Service (pure logic across two Aggregates)

public sealed class TransferService : ITransferService { // Pure domain rule: how to move money between two Accounts public void Transfer(Account from, Account to, Money amount) { if (from.Id == to.Id) throw new DomainException("Cannot transfer to the same account."); if (from.Balance.Currency != to.Balance.Currency || from.Balance.Currency != amount.Currency) throw new DomainException("Currency mismatch."); from.Debit(amount); to.Credit(amount); } }

Why in Domain? The rule (debit A, credit B with validations) is pure business logic and touches multiple Aggregates. It has no infrastructure concerns.



Application Layer (one transaction; orchestrates loading/saving)

public sealed class PaymentsAppService { private readonly IAccountRepository _accounts; private readonly ITransferService _transfer; private readonly IUnitOfWork _uow; // provided by Infrastructure public PaymentsAppService(IAccountRepository accounts, ITransferService transfer, IUnitOfWork uow) { _accounts = accounts; _transfer = transfer; _uow = uow; } public void Transfer(Guid fromId, Guid toId, decimal amount, string currency) { using var tx = _uow.Begin(); // start ACID transaction var from = _accounts.FindById(fromId); var to = _accounts.FindById(toId); _transfer.Transfer(from, to, new Money(amount, currency)); // domain rule _accounts.Save(from); // both saves occur in same transaction _accounts.Save(to); tx.Commit(); // atomic commit or rollback } }

The Application layer coordinates the transaction & persistence. It does not implement the transfer rule; that lives in the Domain Service.



Infrastructure Layer (persistence + transaction)

public interface IUnitOfWork : IDisposable { IUnitOfWork Begin(); void Commit(); void Rollback(); }
  • EfAccountRepository : IAccountRepository implements FindById/Save, honors Version (optimistic concurrency via rowversion or version column).

  • EfUnitOfWork : IUnitOfWork wraps the ORM/DB transaction.

This keeps transaction mechanics and ORM details out of the Domain.


Concurrency & Locking

  • Prefer optimistic concurrency (version check on Save) to avoid unnecessary locks.

  • If the business requires pessimistic locking (e.g., serialized transfers), keep locking inside Infrastructure (repository/UoW), not in the Domain code.

  • On concurrency exceptions, the Application layer decides whether to retry or fail.


Design Checklist (strong consistency across Aggregates)

  1. Challenge the model: should these objects be one Aggregate? (e.g., introduce LedgerEntry as the real Aggregate and keep Account read-model-ish)

  2. If strong consistency is truly required, wrap both modifications in one transaction managed by the Application layer.

  3. Keep the multi-aggregate business rule in a Domain Service (interface + pure implementation in Domain).

  4. Keep repositories & Unit of Work abstractions in Domain, implementations in Infrastructure.

  5. Enforce optimistic concurrency; decide on retries/policies in Application.


When you shouldn’t do this

  • If eventual consistency is acceptable, prefer one Aggregate per transaction + Domain Events (more scalable, simpler).

  • If multi-aggregate modifications become frequent and inevitable, remodel (merge, or introduce a new Aggregate that captures the invariant).


Summary

When you must change multiple Aggregates atomically, keep DDD boundaries intact:

  • Domain: multi-aggregate rule as a Domain Service (pure), plus repository interfaces.

  • Application: transaction orchestration and persistence coordination.

  • Infrastructure: repository + unit of work implementations, concurrency control.

You get strong consistency and a clean, testable model that still respects dependency inversion.

Clean architecture layers



DM: Data Model, DTO: Data Transfer Objects

As presented above, we have 4 layers, in our use case we are going to focus on 3

·       A presentation layer (UI) : that interacts with external requests (e.g. API/ UI).

·       A business layer (application layer): that implements use cases or business logic. This is where your handlers are (if you use CQRS/MediatR), validations and mapping takes place.

·       An infrastructure layer (Data access): This is where you interact with the Database with tools such as DbContext.

Data Transfer Objects

Why Use DTOs?

In a three-layered architecture (Presentation, Business/Domain, Data Access), a key principle is separation of concerns. The presentation layer (UI/API) should not directly depend on the data access layer or domain entities. Doing so would tightly couple the UI to persistence details, making the system fragile and hard to maintain.

To bridge this gap, we use DTOs (Data Transfer Objects).


What Are DTOs?

A DTO is a simple, serializable object whose sole purpose is to carry data between layers or services.

  • Structure: Often resembles entities, but not the same.

  • Fields: May have more, fewer, or reshaped fields compared to domain entities.

  • Behavior: Pure data containers — they do not contain business rules or persistence logic.


Why DTOs Are Important

  1. Layer Separation

    • DTOs decouple internal domain models from external contracts (UI or API).

    • This means internal changes don’t immediately break external consumers.

  2. Controlled Data Exposure

    • DTOs allow selective sharing of information.

    • Example: Excluding sensitive fields like Password or AccountBalance when returning data to the client.

  3. Adaptation for Clients

    • DTOs can reshape or combine data to fit client needs.

    • Example: An OrderDTO might include CustomerName and TotalPrice even though those live in separate objects in the domain model.


Trade-offs

The main drawback of using DTOs is the need to map data between domain entities and DTOs. This mapping adds complexity and boilerplate.

➡️ Solution: Use tools like AutoMapper (C#) or MapStruct (Java) to automate mapping, reduce manual effort, and minimize errors.

Example

Entity:

public class Order
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public decimal Total { get; set; }
public Customer Customer { get; set; } // navigation property
}

DTO for UI:

public class OrderDto
{
public int Id { get; set; }
public string CustomerName { get; set; } // flattened from Customer
public string FormattedDate { get; set; } // custom presentation field
}

👉 The DTO is shaped specifically for what the UI needs, keeping the presentation layer independent from persistence details.


The Responsibility of Each Layer in Generating DTOs

In layered architecture, each layer is responsible for preparing DTOs for the next outer layer:

  1. Data Access Layer → Domain Layer

    • Transforms raw persistence objects (e.g., EF entities, SQL rows) into domain entities.

    • At this level, DTOs are not exposed; instead, the Repository ensures only domain objects cross the boundary.

  2. Domain Layer → Application Layer

    • The domain layer enforces business rules using entities, value objects, and aggregates.

    • The application layer is responsible for mapping domain entities to DTOs that the presentation layer can consume.

    • Example: An Order aggregate in the domain becomes an OrderDTO in the application.

  3. Application Layer → Presentation Layer (API/UI)

    • The application layer returns DTOs to the presentation layer.

    • The presentation layer should never see domain entities directly.

    • Example: An API endpoint /orders/123 returns OrderDTO to the client, not the raw Order entity.



👉 Rule of thumb:

  • Repositories return domain entities, not DTOs.

  • Application services return DTOs to the presentation layer.

  • Presentation layer only consumes DTOs.



Summary:
DTOs ensure that domain logic stays clean and separated from external representation, while giving flexibility to control, adapt, and safely expose data across layers. Each layer is responsible for generating DTOs for the next layer outward — keeping concerns clean and dependencies one-directional.

 

Modules

In Domain-Driven Design (DDD), a Module is a way of grouping related concepts, classes, and tasks into a cohesive unit to reduce complexity in large, evolving systems.

As a domain model grows, it becomes harder to understand and communicate about the whole model. Modules help by:

  1. Organizing related elements into meaningful groups.

  2. Clarifying relationships between concepts.

  3. Reducing mental overload when navigating the system.


Naming Conventions

Modules often follow a hierarchical naming structure, which:

  • Makes logical organization visible in the name.

  • Helps avoid namespace collisions with third-party or external modules.

  • Usually starts with the organization’s name or domain name (often reversed for uniqueness).

Example:

java
com.saasovation
com.saasovation.identityaccess.domain com.saasovation.collaboration.domain com.saasovation.agilepm.domain

In C#, the structure may simply start with the organization’s name:

csharp
SaaSOvation.IdentityAccess.Domain
SaaSOvation.Collaboration.Domain SaaSOvation.AgilePm.Domain

Typical Module Hierarchy

  • Domain Layer Modules:

    • domain.model – Contains entities, value objects, aggregates, reusable interfaces, and abstract classes.

    • domain.service – Contains domain services (if placed outside the model module).

Example:

pgsql
com.saasovation.identityaccess.domain.model
com.saasovation.identityaccess.domain.service
  • Application Layer Modules:

    • Organized by service type if needed (e.g., application.team, application.product).

    • If there are few services, they may remain in the main application module.

Example:

com.saasovation.agilepm.application.team

com.saasovation.agilepm.application.product
  • Presentation Layer Modules:

    • resources – Contains RESTful resource providers.

    • resources.view (or resources.presentation) – Contains pure presentation components.

Example:

pgsql
com.saasovation.agilepm.resources
com.saasovation.agilepm.resources.view

Example: Agile Project Management Context

pgsql
com.saasovation.agilepm.domain.model.product // Aggregate Root: Product com.saasovation.agilepm.domain.model.product.backlogitem // Aggregate Root: BacklogItem com.saasovation.agilepm.domain.model.product.release // Aggregate Root: Release com.saasovation.agilepm.domain.model.product.sprint // Aggregate Root: Sprint

These modules contain the core domain concepts of Scrum for Agile project management.


Key Principles for Using Modules

  1. Group by Concept, Not by Technical Layer – Each module should have a meaningful domain-related name.

  2. Use Hierarchical Naming – Reflect the logical organization in your namespaces or packages.

  3. Modularize Only When Needed – Avoid over-segmentation unless it improves clarity or maintainability.

  4. Separate Concerns Clearly – Domain model, domain services, application services, and presentation resources should be organized distinctly.



 Value Objects

In Domain-Driven Design (DDD), a Value Object represents a descriptive aspect of the domain that has no identity of its own. Instead of being tracked individually, a Value Object is defined entirely by its attributes and the value they represent.

How to Recognize a Value Object

When deciding if a concept should be modeled as a Value Object, check whether it has most of these characteristics:

  • Describes or measures something in the domain (e.g., Money, Address, Weight).

  • Immutable – once created, its state never changes (updates mean replacing it).

  • Conceptual whole – groups related attributes into one meaningful unit.

  • Replaceable – you don’t modify it in place; you swap it with a new one.

  • Equality by values – two instances are equal if all their attributes match.

  • Side-effect free – provides behavior (like formatting or calculations) without altering state.

Example: Address in an E-Commerce System

Imagine an Order entity in an e-commerce system.

The Address is not an entity on its own—it has no identity—but it describes where the order should be shipped.

  • Order (Entity) has an OrderId (identity).

  • Address (Value Object) only matters by its attributes (Street, City, ZipCode, Country).

If a customer moves to a new house, we don’t "update" the old address—we replace it with a new Address object.

Value Object: Address (immutable, equality by value)


public record Address(string Street, string City, string ZipCode, string Country)
{
public override string ToString() => $"{Street}, {City}, {ZipCode}, {Country}";
}

Entity: Order (has identity; contains Address as a Value Object)

public class Order
{
    public Guid OrderId { get; }
    public DateTime OrderDate { get; }
    public string Status { get; private set; }

    // Value Object (no identity; replace when it changes)
    public Address ShippingAddress { get; private set; }

    public Order(Guid orderId, DateTime orderDate, Address shippingAddress)
    {
        OrderId = orderId;
        OrderDate = orderDate;
        ShippingAddress = shippingAddress;
        Status = "Placed";
    }

    public void ShipTo(Address newAddress)
    {
        // Replace the VO atomically (immutability: no in-place mutation)
        ShippingAddress = newAddress;
    }

    public void ShipOrder()
    {
        if (Status != "Placed") 
            throw new InvalidOperationException("Order not in Placed status.");
        Status = "Shipped";
    }
}

Usage(shows equality-by-value & replaceability)


var addr1 = new Address("10 Main St", "Springfield", "12345", "US"); var addr2 = new Address("10 Main St", "Springfield", "12345", "US"); // Value equality: true bool same = addr1 == addr2; var order = new Order(Guid.NewGuid(), DateTime.UtcNow, addr1); // Customer moved → replace the Address with a new instance var newAddr = new Address("55 Elm Ave", "Springfield", "12345", "US"); order.ShipTo(newAddr);

Now, the Address ValueObject has the following characteristics:

  • Descriptive Role: Address describes a customer’s location.
  • Immutability: Being a record in C#, once created it cannot be modified. To “change” an address, you create a new one.
  • Conceptual Whole: Street, City, ZipCode, and Country together form a meaningful unit.
  • Replaceability: If a customer moves, the old Address object is replaced with a new one.
  • Equality by Value: Two addresses with identical fields are equal.
  • Side-Effect-Free Behavior: Its ToString() method produces a representation without altering the object or the system.

Why It Matters

Using Value Objects helps keep models clean, expressive, and safe. They capture concepts from the domain precisely while reducing accidental complexity (no identity tracking, no mutable state). This makes them easy to reason about, test, and use as building blocks inside Entities and Aggregates.

Aggregates

An Aggregate is a cluster of domain objects (Entities and Value Objects) that are treated as a single unit for data changes and consistency. Each Aggregate has a boundary that defines what is inside and outside. Within the boundary:

  • One Aggregate Root (Entity) exists, which is the only entry point from outside.

  • Other Entities inside have local identities meaningful only within the Aggregate.

  • Value Objects and Entities inside can reference each other, but external objects may only reference the Aggregate Root.

The Aggregate Root is responsible for enforcing invariants (business rules that must always remain consistent).


Key Rules for Aggregates

  1. Root-Only Access:
    External objects access the Aggregate only through the root.

  2. Consistency Boundaries:
    Aggregates define transactional consistency boundaries.

    • Within the boundary → transactional (immediate) consistency.

    • Across boundaries → eventual consistency.

  3. One Aggregate per Transaction:
    Only one Aggregate instance should be modified in a single transaction. If multiple need updates, prefer eventual consistency via domain events.

  4. Reference by Identity:
    Aggregates reference other Aggregates only by ID, not by direct object references.

  5. Model True Invariants:
    Aggregates should be shaped around the business rules that must remain consistent.

  6. Design Small Aggregates:
    Favor small Aggregates (often just a Root with Value Objects). Larger Aggregates reduce scalability and performance.


Design Guidelines

  • Aggregate Root Responsibilities:

    • Enforce all business rules (invariants).

    • Control creation, update, and deletion of internal members.

    • Ensure no external object can change internal state directly.

  • Domain Services vs. Aggregates:

    • Aggregates encapsulate rules that fit naturally within one consistency boundary.

    • Use Domain Services for behaviors that span multiple Aggregates.

  • Application Services Role:

    • Orchestrate use cases.

    • Retrieve Aggregates via Repositories.

    • Delegate business logic to Aggregates or Domain Services.


Trade-Offs and Special Cases with Aggregates

1. Large Aggregates → Harder to Scale and Maintain Performance

  • Explanation:
    Large Aggregates contain too many entities and value objects under one root. This makes every transaction heavier, increases lock contention, and reduces scalability.

  • Example:
    Imagine an E-commerce “Order” Aggregate that includes not only the Order root but also every OrderItem, the PaymentTransaction, the Shipment, and even the Invoice as part of the same Aggregate.
    Any update (like changing the shipping address) would lock the entire Aggregate, creating performance bottlenecks.
    Better Approach: Split it into smaller Aggregates — e.g., Order, Payment, and Shipment — each with its own root.


2. Eventual Consistency → Recommended When Multiple Aggregates Need Coordination

  • Explanation:
    Instead of forcing multiple Aggregates to update in a single transaction (which is costly and violates the one-aggregate-per-transaction rule), use domain events to achieve eventual consistency.

  • Example:
    In the food delivery domain:

    • The Order Aggregate is updated when a customer places an order.

    • It then publishes an OrderPlaced event.

    • The Restaurant Aggregate subscribes to that event and updates its preparation queue.
      Both Aggregates remain consistent eventually, without being locked in the same transaction.




3. Breaking the Rules May Be Justified in Special Cases

a. Batch Creation in the UI (When Invariants Are Not at Risk)

  • Explanation:
    Sometimes the UI allows users to create multiple objects at once (batching). If each object is a valid Aggregate by itself and doesn’t depend on others for invariants, batching is acceptable.

  • Example:
    A Project Management tool allows a user to create 10 Backlog Items at once. Each BacklogItem is its own Aggregate, so whether created individually or in batch, invariants remain intact.


b. Lack of Infrastructure (No Messaging/Event Mechanisms)

  • Explanation:
    Eventual consistency usually depends on messaging systems or async processing. If the project lacks this infrastructure, developers may need to temporarily update multiple Aggregates in one transaction.

  • Example:
    In a legacy system without message queues, updating both Customer and Invoice in the same transaction may be unavoidable to ensure payment records stay in sync.


c. Enterprise Constraints (Global Transactions Enforced)

  • Explanation:
    Some enterprises mandate strict two-phase commit transactions across services for auditing or policy reasons. This forces multiple Aggregates into the same transaction even though it hurts scalability.

  • Example:
    A Banking system requires Account and Ledger entries to be updated atomically in the same global transaction for compliance.


d. Performance (Using Direct References for Queries)

  • Explanation:
    Normally, Aggregates should reference others only by identity. But for performance, sometimes direct references are added to avoid expensive joins.

  • Example:
    In a CRM system, if Customer needs to frequently display their PreferredAddress, storing a direct reference instead of just an ID may improve query performance. This must be balanced carefully with design principles.


Example

Imagine a Customer Aggregate:

  • Customer (Root) – global identity, enforces invariants.

  • Address (Value Object) – belongs inside the Aggregate.

  • PaymentMethod (Entity with local identity) – accessible only through Customer.

External systems can only reference the Customer root. If they need the Address, a copy is returned, not a direct reference.


Perfected Definition:
Aggregates are consistency boundaries in a domain model. They group related entities and value objects under a single root entity that controls access and enforces business invariants. Aggregates ensure transactional consistency internally, reference other Aggregates only by identity, and should be kept small to maximize scalability. Eventual consistency should be used across Aggregate boundaries, with Domain Events coordinating changes.

Factories

A Factory in Domain-Driven Design (DDD) is responsible for creating complex domain objects or Aggregates in a controlled and expressive way. Unlike simple object instantiation, Factories ensure that creation logic respects business rules, invariants, and the Ubiquitous Language of the domain.

Factories can appear in two main forms:

  1. Aggregate Factories (Factory Methods): When an Aggregate Root provides methods to create new instances of entities or Aggregates.

  2. Service-Based Factories: When a separate Domain Service is responsible for translating o constructing objects, often across Bounded Contexts.




Factory Methods in Aggregates

Sometimes, the creation of an object is not just a technical detail but a business concept. In those cases, the Aggregate Root exposes a Factory Method.

Example – Calendar Aggregate:

java public CalendarEntry scheduleCalendarEntry(
CalendarEntryId entryId, Owner owner, String subject, String description, TimeSpan timeSpan, Alarm alarm, Repetition repetition, String location, Set<Invitee> invitees) { CalendarEntry entry = new CalendarEntry( this.tenant(), this.calendarId(), entryId, owner, subject, description, timeSpan, alarm, repetition, location, invitees ); DomainEventPublisher.instance() .publish(new CalendarEntryScheduled(...)); return entry; }

Here, the Calendar Aggregate Root is more than just a data container. It actively controls creation of CalendarEntry, ensures correct initialization, and raises a domain event (CalendarEntryScheduled).


Example – Forum Aggregate:

java
public Discussion startDiscussion(
DiscussionId discussionId, Author author, String subject) { if (this.isClosed()) { throw new IllegalStateException("Forum is closed."); } Discussion discussion = new Discussion( this.tenant(), this.forumId(), discussionId, author, subject ); DomainEventPublisher.instance() .publish(new DiscussionStarted(...)); return discussion; }

Here, Forum.startDiscussion() enforces the rule that no discussion can be started if the forum is closed, while still creating the Discussion object.
Notice how the method name directly expresses the Ubiquitous Language: “Authors start discussions on forums.”


Service-Based Factories

Sometimes, creating a domain object requires translation from another Bounded Context. In such cases, a Domain Service can act as a Factory.

Example – UserRoleToCollaboratorService:
This service translates user identities from the Identity & Access Context into corresponding Collaboration Context roles (Author, Creator, Moderator, etc.).

java
public class UserRoleToCollaboratorService implements CollaboratorService {
@Override public Author authorFrom(Tenant tenant, String identity) { return (Author) UserInRoleAdapter .newInstance() .toCollaborator(tenant, identity, "Author", Author.class); } // other factory methods... }

And the domain interface:

java public interface CollaboratorService {
Author authorFrom(Tenant tenant, String identity); Creator creatorFrom(Tenant tenant, String identity); Moderator moderatorFrom(Tenant tenant, String identity); Owner ownerFrom(Tenant tenant, String identity); Participant participantFrom(Tenant tenant, String identity); }

This design clearly separates life cycles and concepts between contexts while preserving clarity in the domain language.


Wrap-Up

  • Factories make models more expressive and aligned with the Ubiquitous Language.

  • Factory Methods in Aggregates enforce invariants while creating new objects.

  • Service-Based Factories handle object creation that requires cross-context translation.

  • This separation ensures correctness, clarity, and maintainability in the domain model.


Repositories

In Domain-Driven Design (DDD), a Repository is a mechanism for accessing, retrieving, and persisting Aggregates.

  • Each persistent Aggregate type typically has its own Repository (one-to-one relationship).

Example: OrderRepository for Order Aggregate, CustomerRepository for Customer Aggregate.
This ensures clear separation and aligns with the idea of “one Repository per Aggregate.”

  • In some cases, when two or more Aggregates share the same hierarchy, they may share a single Repository.

This happens when:

The Aggregates are closely related and stored in the same persistence structure.
They share a natural lifecycle (e.g., always created, loaded, or queried together).

    1. The boundary between them is more technical than conceptual, so separating Repositories would add unnecessary complexity.

Example: Financial Accounts

BankAccount (Aggregate Root) and TransactionHistory (could be another Aggregate Root).
Transactions are always tied to an account and stored together.
    • So instead of two repositories, a single AccountRepository manages both.

Why Do This?

Reduce redundancy: Prevents duplicate persistence logic.
Consistency: Ensures Aggregates that always live together are persisted together.
Performance: Avoids multiple repository calls when they always belong to the same hierarchy.
Repositories abstract away persistence concerns, allowing the domain layer to work with pure domain objects without depending on the database or ORM technology.

Without the Repository Pattern

When using an ORM directly (e.g., DbContext in Entity Framework Core):

  • The application layer depends directly on persistence APIs.

  • This creates several issues:

  1. Testability – Difficult to test DbContext without database dependencies.

  2. Repetitive Code – Each DbSet leads to repetitive CRUD logic.

  3. Tight Coupling – Application layer depends on the ORM (violating Dependency Inversion).

  4. ORM Lock-in – Switching from EF Core to another ORM (like Dapper) requires major code changes.


Why Use the Repository Pattern?

The Repository Pattern introduces an abstraction (interface) between domain logic and persistence.

Benefits:

  • Testability: Repository interfaces allow mocking in unit tests.

  • DRY Principle: Reusable repository abstractions reduce boilerplate code.

  • Separation of Concerns: Keeps business logic independent from persistence.

  • ORM Flexibility: Changing ORM implementations affects only the Repository layer.


Disadvantages

As with any abstraction, Repositories come with trade-offs:

  1. Added Complexity: Introduces an additional layer.

  2. Orchestration: Interfaces must be correctly implemented and registered in DI.

  3. Mapping Overhead: Requires DTO–Entity mapping; tools like AutoMapper may not always work with generic expressions.

  4. Transaction Management: Must coordinate persistence across multiple Repositories (often using a Unit of Work).


Repository Design Approaches

1. Collection-Oriented Repositories

  • Behave like an in-memory Set or Collection.

  • Hide all persistence details (no explicit save() calls).

  • Adding an object means it is automatically tracked.

  • Modification is performed by retrieving the object, applying commands, and committing the transaction.

Example (Calendar Repository):

java
public class CalendarRepository {
private Map<CalendarId, Calendar> calendars = new HashMap<>(); public void add(Calendar calendar) { this.calendars.put(calendar.calendarId(), calendar); } public Calendar findCalendar(CalendarId calendarId) { return this.calendars.get(calendarId); } }

Usage:

java CalendarId calendarId = new CalendarId(...);
Calendar calendar = new Calendar(calendarId, "Project Calendar", ...); CalendarRepository repo = new CalendarRepository(); repo.add(calendar); // Modify through behavior, not re-save Calendar toRename = repo.findCalendar(calendarId); toRename.rename("CollabOvation Project Calendar");

Persistence backends (e.g., Hibernate, EF Core) must track changes automatically via:

  • Copy-on-Read: Compare loaded vs. modified copies at commit.

  • Copy-on-Write: Use proxies that track changes and flush them at commit.


2. Persistence-Oriented Repositories

  • Explicitly expose persistence operations like save(), update(), delete().

  • More practical when underlying ORMs require explicit save commands.


Implementation Steps

  1. Define a Repository Interface in the same module as the Aggregate type.

    csharp
    void add(CalendarEntry entry); void remove(CalendarEntry entry); CalendarEntry calendarEntryOfId(Tenant tenant, CalendarEntryId id); ICollection<CalendarEntry> entriesOfCalendar(Tenant tenant, CalendarId id); CalendarEntryId nextIdentity(); }
  2. public interface CalendarEntryRepository {
  3. Provide an Implementation in the infrastructure layer (preferred).

    csharp public class HibernateCalendarEntryRepository
    : CalendarEntryRepository { // Hibernate/EF Core persistence logic here }

Summary

  • Repositories decouple domain logic from persistence and make testing, maintenance, and ORM replacement easier.

  • Choose between collection-oriented (abstracting persistence completely) and persistence-oriented (exposing save operations) based on project needs.

  • Use a Unit of Work to coordinate transactions across multiple Repositories.

Application Layer

Delegation from Presentation Model

The Presentation Model should not take on the responsibility of orchestrating complex business operations. It must not act as an Application Service itself. Instead, its role should be limited to delegating tasks to an Application Service, such as BacklogItemApplicationService, which serves as a Facade to handle more complex use case flows.

Application Layer Responsibilities — In Detail

1) Use cases (orchestrates the flow)

What: Each public method in the Application layer represents a use case (e.g., PlaceOrderTransferFundsShipOrder).
Job: Coordinate steps end-to-end:

  • Validate request shape (not deep domain rules).

  • Load Aggregates via Repositories (interfaces).

  • Call domain logic (Aggregates / Domain Services).

  • Save changes.

  • Publish domain/outbox events if needed.

  • Return a DTO to callers.

Why here: Keeps the “story” of the operation in one place; the Domain stays focused on local invariants.


2) Transactions / Unit of Work (UoW)

What: The use case defines the transaction boundary (begin/commit/rollback). The UoW is an abstraction that wraps the DB transaction (EF DbContext / Hibernate Session / raw SQL).
How:

  • Start transaction at the beginning of the use case.

  • Save all modified Aggregates.

  • Commit once at the end (or rollback on failure).

  • For events, use the Outbox pattern to persist events atomically with the transaction, then dispatch asynchronously.

Why here: Only the Application layer can see all the steps; the Domain must remain persistence-agnostic.


3) Security

What: Enforce authentication (who are you?) and authorization (what are you allowed to do?) for each use case.
How:

  • Check policies (e.g., roles/claims/ownership) before calling domain logic.

  • Keep security out of Aggregates; pass only what the domain needs (e.g., CustomerId).

Why here: Security is a cross-cutting concern tied to the user/request, not to domain invariants.


4) Retries

What: Transient failures (network hiccups, deadlocks) can be retried safely at the Application layer.
How:

  • Use retry policies (exponential backoff; e.g., Polly in .NET).

  • Only retry idempotent operations (or pair with an idempotency key, below).

  • Don’t retry business rule violations—only technical transient errors.

Why here: The Application layer sees infrastructure exceptions and can decide policy without polluting domain code.


5) Idempotency

What: Running the same request more than once should not apply the change multiple times (important for payments, transfers, webhooks).
How:

  • Require an Idempotency-Key (e.g., header) from clients.

  • Store keys with outcome; if the same key appears again, return the original result instead of performing the operation again.

  • Combine with retries for at-least-once delivery scenarios.

Why here: Idempotency is a property of the use case execution, not a single Aggregate method.


6) Mapping to DTOs

What: Translate between external shapes (request/response DTOs) and internal domain types (Aggregates, Value Objects).
How:

  • Input DTOs/Commands → validate → map to domain calls.

  • Output DTOs/View Models ← map from domain results.

  • Do not expose Entities/Aggregates directly to clients.

Why here: Keeps the Domain model pure and lets the API evolve safely without breaking internal code.


Mini Example (C#-style pseudo-code)

strongly consistent money transfer use case showing all concerns together:


Where things live:

  • Domain layerAccount aggregate + IAccountRepository interface + Money VO.

  • Application layer: this TransferFundsUseCase, transaction choice, security check, retry policy, idempotency logic, DTO mapping.

  • Infrastructure layer: repository implementation, IUnitOfWork implementation, IIdempotencyStore implementation, IAuthorization adapter, retry library wiring.


Practical Tips

  • Keep use cases thin but complete: all steps for a business action in one place.

  • One Aggregate per transaction when possible; if not, declare strong consistency here (as shown) and accept throughput trade-offs.

  • Outbox pattern for reliable event publication.

  • Centralized DTO mappers (e.g., MapStruct, AutoMapper) help, but don’t hide domain concepts.

  • Log: idempotency key, correlation id, user, and outcome for auditability.

This approach makes your Application layer the clear owner of execution policy, while your Domain stays clean and focused on business rules.


Domain Payload Objects (DPO)

An alternative to DTOs is the Domain Payload Object (DPO) [Vernon].

  • What it does: A DPO groups whole Aggregate instances into a single container, allowing multiple Aggregates to be passed between layers for view rendering.

  • How it works:

    • The Application Service retrieves Aggregates via Repositories.

    • It instantiates the DPO and places Aggregate references inside.

    • The presentation layer retrieves Aggregates from the DPO and queries their attributes for display.

Drawbacks:

  • Aggregates must expose state for reading, which risks coupling the UI to the model. This can be mitigated using MediatorsDouble Dispatch, or an Aggregate Root query interface.

  • Since DPOs hold Aggregate references, unresolved lazy-loaded collections can cause runtime errors once the Application Service transaction is closed.

    • Solutions: Use eager loading or apply a Domain Dependency Resolver (DDR) strategy per use case.


1. State Representations in REST

Clear Definition

When building REST APIs, we should not expose domain Aggregates directly.

  • Aggregates represent the internal business model.

  • If we return them as-is (1:1 mapping), clients will need to understand internal rules, structures, and relationships — which couples them too tightly to the domain model.

Instead, REST responses should be designed as use-case-specific representations, also known as Presentation Models. These models show only the data that is necessary for the client’s interaction (nothing more, nothing less).


Example – Bad Approach (Direct Aggregate Mapping)

Imagine an Order Aggregate:

json
{
"orderId": "123",
"customer": {
"id": "c001",
"name": "Alice",
"loyaltyPoints": 240
},
"orderItems": [
{ "productId": "p001", "name": "Laptop", "price": 1200, "internalStockCode": "STK-987" }
],
"status": "PENDING",
"createdAt": "2025-08-18T10:30:00Z"
}

Problems:

  • It exposes internal attributes (internalStockCode, loyaltyPoints).

  • It forces the client to understand the Order Aggregate structure.


Example – Good Approach (Use-Case-Specific Representation)

If the client only needs to display a summary of orders in a mobile app, the REST resource might look like:

json
{
"orderId": "123",
"customerName": "Alice",
"totalAmount": 1200,
"status": "PENDING"
}

Benefits:

  • Only includes what’s necessary for this use case.

  • Hides internal details.

  • Makes the API easier to evolve without breaking clients.


2. Use Case Optimal Repository Queries

Clear Definition

Normally, Application Services fetch Aggregates through Repositories and then assemble DTOs or DPOs (Domain Payload Objects). This means extra mapping and orchestration code.

Use Case Optimal Repository Query avoids this by:

  • Designing query methods directly in the Repository that return Value Objects shaped specifically for the use case.

  • These Value Objects are domain-specific, not DTOs.

  • This is similar to CQRS but still works against the same domain persistence store (no separate read database required).


Example – Normal Repository Query (Extra Work in Service Layer)


Here, the Application Service has to manually assemble the WeeklySchedule.


Example – Use Case Optimal Query (Better)

Define the query inside the Repository:


Repository implementation directly queries persistence and returns the Value Object:


Usage:


Benefits:

  • The Repository handles orchestration.

  • The service layer just delegates, making it thinner.

  • The WeeklySchedule Value Object is shaped perfectly for the use case (reporting weekly schedules).


✅ In summary

  • REST State Representations: Always return use-case-focused representations, not raw Aggregates.

  • Use Case Optimal Queries: Move query logic into Repositories so they can return domain Value Objects directly shaped for the use case, reducing boilerplate in Application Services.

Supporting Multiple, Disparate Clients

Applications often serve multiple client types (e.g., RIA, thick clients, REST APIs, messaging, automated tests).

  • One solution is to use Data Transformers.

  • Application Services accept a Data Transformer parameter and double-dispatch based on it to produce the required format.

Example:

csharp CalendarWeekData calendarWeekData =
calendarAppService.calendarWeek(date, new CalendarWeekXMLDataTransformer());

Possible implementations of CalendarWeekDataTransformer:

  • CalendarWeekCSVDataTransformer

  • CalendarWeekDPODataTransformer

  • CalendarWeekDTODataTransformer

  • CalendarWeekJSONDataTransformer

  • CalendarWeekTextDataTransformer

  • CalendarWeekXMLDataTransformer

This allows one Application Service to serve multiple client needs consistently.

Minimal structure (namespaces)

  • Application

    • Application.Calendar.ICalendarWeekDataTransformer

    • Application.Calendar.CalendarAppService

    • Application.Calendar.CalendarWeekData (output wrapper)



  • Presentation/API (using InterfaceAdapters)

    • InterfaceAdapters.Calendar.CalendarWeekCSVDataTransformer

    • InterfaceAdapters.Calendar.CalendarWeekJSONDataTransformer

    • InterfaceAdapters.Calendar.CalendarWeekXMLDataTransformer




  • Domain

    • Domain.Calendar.ICalendarRepository

    • Domain.Calendar.CalendarEntry

  • Infrastructure

    • Infrastructure.Calendar.EfCalendarRepository (or InMemory)

Extra tip: you might not need custom transformers

If your API only serves JSON, you can return an output DTO from the Application layer and let the web framework serialize it automatically. Add CSV/XML transformers only if you truly need multiple media types or special shapes.


Bottom line:

  • Keep format-specific code (CSV/JSON/XML) in Interface Adapters.

  • Keep use-case orchestration and the transformer interface in Application.

  • Keep domain rules and repository interfaces in Domain.


Role of Application Services

  • Application Services are the direct clients of the domain model.

  • Responsibilities:

    • Orchestrating use case flows (one method per flow).

    • Managing transactions (ACID guarantees).

    • Enforcing security.

    • Delegating business operations to the domain model (Aggregates, Value Objects, Domain Services).

Important Distinction:

  • Application Services ≠ Domain Services.

    • Application Services handle orchestration.

    • Domain Services contain domain business logic.

  • Keep Application Services thin; the bulk of business logic belongs inside the domain layer.


Command Objects

When Application Service methods take many parameters, the method signature becomes noisy and hard to maintain.

  • A better approach is to use a Command Object [Gamma et al.].

  • A Command encapsulates all parameters into a single object that represents a business request.

  • This makes methods cleaner, supports queuing, logging, or even undo operations.


Composing Multiple Bounded Contexts

User interfaces often combine multiple models (e.g., ProductsDiscussionsReviews) into a unified view.

Two approaches:

  1. Multiple Application Layers (portal/portlet style):

    • Each UI component interacts with its own Application Layer tied to a specific Bounded Context.

    • Downside: harder to harmonize across use case flows.

  2. Single Application Layer (preferred):

    • Provides a unified source for composition.

    • Aggregates and services from different contexts are combined here into cohesive results for the UI.

    • This layer contains no business logic—it only aggregates and adapts.


✅ Summary

  • Presentation Model delegates to Application Services (no heavy lifting in the UI).

  • DPOs allow passing Aggregates together, but require careful handling of lazy loads.

  • REST endpoints should expose use-case-specific representations, not raw Aggregates.

  • Use case optimal queries and Value Objects simplify views and reduce orchestration complexity.

  • Data Transformers support multiple clients consistently.

  • Application Services orchestrate use cases, transactions, and security while staying thin and delegating business rules to the domain model.

  • Command objects prevent bloated method signatures.

  • For cross-context composition, a single Application Layer often works best.


Domain Services

1. What a Domain Service Is

A Domain Service is a stateless operation that belongs to the domain model, expressed in the Ubiquitous Language.

It represents a business concept that:

  • Does not naturally fit into a single Aggregate, Entity, or Value Object.

  • Often involves multiple Aggregates.

  • Encapsulates domain rules, processes, or transformations.

Typical Uses

  • Perform a significant business process
    Example: TransferMoneyService moves funds between two Account Aggregates.

  • Transform domain objects
    Example: SchedulingService converts a draft MeetingRequest into a confirmed CalendarEntry.

  • Calculate complex values
    Example: BurndownChartCalculator computes metrics across multiple Sprint Aggregates in Agile project management.


2. What a Domain Service Is Not

  • ❌ Not an Application Service (no orchestration, transactions, or external I/O).

  • ❌ Not an infrastructure service (doesn’t talk to databases, HTTP, or messaging).

  • ❌ Not a replacement for entity behavior (most business logic should still live inside Aggregates).

⚠️ Overusing domain services risks creating an Anemic Domain Model, where entities are reduced to data containers and all logic lives in services.


3. Guidelines for Using Domain Services

  • ✅ Use sparingly; prefer putting logic in Entities and Value Objects.

  • ✅ Always name them in the Ubiquitous Language (PricingPolicyService, TransferMoneyService).

  • ✅ Keep them stateless – no internal state, only input/output behavior.


4. When a Domain Service Uses Multiple Aggregates

Sometimes a business rule spans more than one Aggregate and doesn’t fit neatly inside either Aggregate’s root.

Rules:

  • The service can read multiple Aggregates.

  • Modify only one Aggregate per transaction.

  • Use Domain Events to coordinate changes across Aggregates (eventual consistency).

  • Let the Application Layer load Aggregates via repositories and pass them into the service.


5. Service Interface & Layer Placement

Domain Layer

  • Defines interfaces and pure implementations.

  • Examples:

    • IPricingPolicyService

    • DefaultPricingPolicyService

Infrastructure Layer

  • Implements external adapters.

  • Examples:

    • SqlOrderRepository

    • HttpExchangeRateProvider

Application Layer

  • Orchestrates transactions, loads aggregates, and calls domain services.

  • Example: OrderApplicationService calls IPricingPolicyService.

Rule of Thumb:

  • Pure business logic → stays in the Domain Layer.

  • External dependencies → interface in Domain, implementation in Infrastructure.

  • Orchestration logic → Application Layer.


6. Example: PricingPolicyService

Domain Layer — Service Interface

public interface IPricingPolicyService { Money PriceFor(Product product, Customer customer, OrderContext context); }

Domain Layer — Pure Implementation

public sealed class DefaultPricingPolicyService : IPricingPolicyService { public Money PriceFor(Product product, Customer customer, OrderContext context) { var basePrice = product.BasePrice; decimal discount = 0m; if (customer.LoyaltyTier == LoyaltyTier.Gold) discount += 0.10m; if (context.Season == Season.BlackFriday) discount += 0.15m; var discounted = basePrice.Amount * (1 - discount); return new Money(discounted, basePrice.Currency); } }

Application Layer — Orchestration

public sealed class OrderApplicationService { private readonly IProductRepository _products; private readonly ICustomerRepository _customers; private readonly IPricingPolicyService _pricing; public OrderApplicationService( IProductRepository products, ICustomerRepository customers, IPricingPolicyService pricing) { _products = products; _customers = customers; _pricing = pricing; } public QuoteDto GetQuote(Guid productId, Guid customerId, OrderContext context) { var product = _products.FindById(productId); var customer = _customers.FindById(customerId); var price = _pricing.PriceFor(product, customer, context); return new QuoteDto(productId, customerId, price.Amount, price.Currency); } }

7. Example: OrderPaymentService

The OrderPaymentService handles charging a customer for an order once the restaurant accepts it.
This involves multiple Aggregates and an external payment gateway → a Domain Service.

Domain Layer — Interface

public interface IOrderPaymentService { void PayOrder(Guid orderId, PaymentDetails details); }

Application Layer — Implementation with External Gateway

public sealed class OrderPaymentService : IOrderPaymentService { private readonly IOrderRepository _orders; private readonly IPaymentGateway _gateway; public OrderPaymentService(IOrderRepository orders, IPaymentGateway gateway) { _orders = orders; _gateway = gateway; } public void PayOrder(Guid orderId, PaymentDetails details) { var order = _orders.GetById(orderId); if (!order.IsAccepted) throw new InvalidOperationException("Order not accepted"); var result = _gateway.Charge(details, order.TotalAmount); if (!result.Success) throw new InvalidOperationException("Payment failed"); order.MarkAsPaid(result.TransactionId); _orders.Save(order); } }

8. Example: FundTransferService (Strong Consistency Across Aggregates)

Some business rules require strong consistency across Aggregates (e.g., fund transfers).
This can be enforced with a Unit of Work (UoW).

Domain Layer — Interface

public interface IFundTransferService { void Transfer(Guid fromAccountId, Guid toAccountId, Money amount); }

Application Layer — Implementation with UoW

public sealed class FundTransferService : IFundTransferService { private readonly IAccountRepository _accounts; public FundTransferService(IAccountRepository accounts) { _accounts = accounts; } public void Transfer(Guid fromAccountId, Guid toAccountId, Money amount) { using (var uow = _accounts.BeginUnitOfWork()) { var from = _accounts.GetById(fromAccountId); var to = _accounts.GetById(toAccountId); from.Withdraw(amount); to.Deposit(amount); _accounts.Save(from); _accounts.Save(to); uow.Commit(); } } }

9. Quick Placement Guide

  • IPricingPolicyService (interface) → Domain

  • DefaultPricingPolicyService (pure impl) → Domain

  • IExchangeRateProvider (interface) → Domain

  • HttpExchangeRateProvider (impl) → Infrastructure

  • OrderApplicationService (orchestration) → Application

  • Repositories (interfaces) → Domain

  • Repository implementations → Infrastructure


✅ Summary

  • Domain Services capture business rules that don’t belong to one Aggregate.

  • They should stay stateless, pure, and named in Ubiquitous Language.

  • Keep the Domain clean: external dependencies are defined as interfaces in Domain, implemented in Infrastructure.

  • Use Application Services to orchestrate, load Aggregates, and call Domain Services.

  • For multi-aggregate updates, prefer eventual consistency with Domain Events. Use strong consistency only when absolutely necessary.


Aggregates and Event Sourcing

What is Event Sourcing?

Event Sourcing is a design pattern where the state of an Aggregate (10) is not stored directly as a snapshot, but instead captured as a series of domain Events (8). Each Event represents something that has happened in the past (e.g., “TaskCreated”, “TaskAssigned”, “TaskCompleted”).

When you need the current state of an Aggregate, you don’t fetch a stored object. Instead, you replay its Events in the order they occurred, rebuilding the state step by step.


Event Store (8)

An Event Store is a specialized persistence mechanism used in Event Sourcing. Unlike a traditional database table that stores only the latest state of each entity, the Event Store records every Event that has ever occurred for an Aggregate.

  • Each Aggregate has its own Event Stream, identified by the Aggregate Root’s identity.

  • The Event Stream is an ordered log of domain Events.

  • The Event Store acts as the single source of truth because it contains the complete history of all changes.

Clearer Explanation of Source of Truth
Instead of relying on a single snapshot of an object, the Event Store ensures you always have the full history. From this history, you can always derive the current state by replaying Events.

Example:
Imagine a BankAccount Aggregate. Instead of storing only the current balance, the Event Store saves Events like:

  • AccountOpened($0)

  • DepositMade($100)

  • WithdrawalMade($30)

If you replay these Events, you derive the current balance: $70. Even if you never stored “$70” explicitly, you can compute it at any time from the Event Store.

Why Use Event Sourcing?

  • Auditability: You can always track what happened, when it happened, and who performed the action.

  • Debugging & Replay: You can replay historical Events to simulate or debug system behavior.

  • Flexibility: You can project different views of the same data without altering the core model.

1. Auditability and Traceability

Every change in the system is explicitly recorded as an event.

  • You know when something happened, what happened, and why.

  • Perfect for financial systems, healthcare records, or compliance-heavy industries.

Example: If a patient’s medical record changes, you can trace exactly when a diagnosis was added, modified, or removed.


2. Flexibility

From the same stream of events, you can project different views depending on the use case — without altering the domain model.

Example: In an e-commerce system, an Order Aggregate may generate this event stream:

OrderPlaced (OrderId=123, Total=$150)
ItemAdded (OrderId=123, Item=Shoes, $100) ItemAdded (OrderId=123, Item=Belt, $50) OrderShipped (OrderId=123, Date=2025-08-19)

From this:

  • The Customer Order History view shows placed and shipped dates.

  • The Financial Summary view shows total revenue from the order.

  • The Inventory view shows which items should be deducted from stock.

All three views come from the same data source — the event stream — without modifying the core model. Each view subscribes to only the specific events it cares about, ensuring that it stays relevant to its own use case while ignoring unnecessary events.

Example:

  • A Customer View might subscribe to events such as CustomerRegistered and CustomerAddressChanged.

  • An Order View might subscribe only to OrderPlaced, OrderShipped, and OrderCancelled.

  • A Billing View could subscribe to InvoiceGenerated and PaymentReceived.

This way, every view is powered by the same underlying event stream but only reacts to the subset of events that matter to it.


3. Scalability and Projections

Since events are stored as an append-only log, you can scale read models independently. Different consumers (dashboards, analytics engines, audit services) can subscribe to the event stream and build their own projections.

Example: A shipping service could subscribe only to OrderShipped events, while the accounting service subscribes to financial transaction events.




REST and DDD – Why Not Expose Aggregates Directly?

It might be tempting to expose your domain model directly as REST resources (e.g., returning Aggregates as JSON). However, this approach is brittle because every change in the domain model leaks into the API.

❌ Bad Approach: Exposing the Aggregate Directly

http
GET /users/123/tasks/456

Response (returns the full Task Aggregate):

json
{
"taskId": 456, "title": "Prepare Monthly Report", "description": "Monthly financial and performance report for August", "status": "In Progress", "dueDate": "2025-09-01", "assignedUser": { "id": 123, "name": "John Doe", "email": "john.doe@example.com" }, "auditLogs": [ { "changedBy": "system", "timestamp": "2025-08-10T14:23:00Z", "change": "created" }, { "changedBy": "jane.doe", "timestamp": "2025-08-12T09:15:00Z", "change": "updated deadline" } ], "internalWorkflowState": "VALIDATED_PENDING_SYNC", "lastModifiedBySystem": "scheduler-service" }
  • This response exposes every internal detail of the Task Aggregate, including audit logs and workflow states that clients should never depend on.

  • If the domain team renames dueDatedeadline or removes internalWorkflowState, all the users (clients) who use the API will now get errors because their applications still expect dueDate or internalWorkflowState to exist, even though these changes are small and internal.


✅ Good Approach: Use Case–Specific Representation

http
GET /users/123/tasks/456/summary

Response (focused, stable view):

json
{ "taskId": 456, "title": "Prepare Monthly Report", "status": "In Progress", "dueDate": "2025-09-01" }
  • Clients see only what’s relevant to their use case.

  • Internal model changes (audit logs, workflow states, system fields) don’t affect this contract.

  • The API remains stable and easy to evolve.


👉 This side-by-side example makes it clear why exposing the full Aggregate is fragile, while a use-case-driven response protects both the domain model and API clients.


In summary:

  • Event Sourcing represents Aggregates as a sequence of events, stored in an Event Store.

  • The Event Store is the source of truth, and you can rebuild state or project new views anytime.

  • REST interfaces should not expose Aggregates directly but should provide use-case-specific representations, ensuring clients remain decoupled from domain internals.

Comments

Popular posts from this blog

Maxpooling vs minpooling vs average pooling

Generative AI - Prompting with purpose: The RACE framework for data analysis

Best Practices for Storing and Loading JSON Objects from a Large SQL Server Table Using .NET Core