Skip to main content

Overview

Domain-Driven Design (DDD) is a software development approach that emphasizes deep collaboration between technical and domain experts to create software that accurately reflects business reality. Intent Architect provides comprehensive support for DDD patterns through its Domain Designer and various modules.

DDD Core Principles

Focus on the core domain and domain logic, use models to solve complex business problems, and maintain a ubiquitous language shared between developers and domain experts.

Building Blocks

Intent Architect supports all the tactical patterns of DDD through code generation:

Entities

Entities are objects with a distinct identity that persists throughout their lifecycle. Module: Intent.Entities
[IntentManaged(Mode.Merge, Signature = Mode.Fully)]
public class Order
{
    public Guid Id { get; set; }  // Identity
    public string OrderNumber { get; set; }
    public DateTime OrderDate { get; set; }
    public Guid CustomerId { get; set; }
    public OrderStatus Status { get; private set; }
    
    // Rich behavior - not just data
    public void MarkAsShipped()
    {
        if (Status != OrderStatus.Confirmed)
            throw new InvalidOperationException(
                "Only confirmed orders can be shipped");
        
        Status = OrderStatus.Shipped;
        // Domain event could be raised here
    }
}
Key Characteristics:
  • Unique Identity: Distinguished by ID, not attributes
  • Mutable State: Can change over time
  • Rich Behavior: Encapsulates business logic
  • Lifecycle Management: Created, modified, deleted
Two entities are equal if they have the same identity, even if all their attributes differ.

Value Objects

Value Objects are immutable objects defined by their attributes, with no conceptual identity. Module: Intent.ValueObjects
public class Address
{
    public Address(string street, string city, string postalCode, string country)
    {
        Street = street;
        City = city;
        PostalCode = postalCode;
        Country = country;
    }

    public string Street { get; }
    public string City { get; }
    public string PostalCode { get; }
    public string Country { get; }

    // Value objects are compared by their attributes
    public override bool Equals(object obj)
    {
        if (obj is not Address other) return false;
        return Street == other.Street &&
               City == other.City &&
               PostalCode == other.PostalCode &&
               Country == other.Country;
    }
}
Key Characteristics:
  • Immutable: Once created, cannot be changed
  • No Identity: Defined entirely by attributes
  • Interchangeable: Two value objects with same attributes are equal
  • Side-Effect Free: Operations return new instances

Aggregates and Aggregate Roots

Aggregates are clusters of domain objects treated as a single unit, with an Aggregate Root controlling access.
public class Order  // Aggregate Root
{
    public Guid Id { get; set; }
    public string OrderNumber { get; set; }
    
    // Private backing field - encapsulation
    private readonly List<OrderItem> _items = new();
    
    // Public read-only access
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    // All modifications go through the aggregate root
    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        // Business rule: cannot add items with zero quantity
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
        
        var existingItem = _items.FirstOrDefault(i => i.ProductId == productId);
        if (existingItem != null)
        {
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            var newItem = new OrderItem(productId, quantity, unitPrice);
            _items.Add(newItem);
        }
    }

    public void RemoveItem(Guid productId)
    {
        var item = _items.FirstOrDefault(i => i.ProductId == productId);
        if (item != null)
        {
            _items.Remove(item);
        }
    }
}
Modeling Aggregates in Intent Architect: In the Domain Designer, use composition (black diamond) to model ownership:
Order (Aggregate Root)
  └─── OrderItem (owned entity)
  └─── ShippingAddress (owned value object)
Aggregates define transaction boundaries. All changes within an aggregate are saved together in a single transaction.

Domain Events

Domain Events represent something significant that happened in the domain. Module: Intent.DomainEvents
public class OrderPlacedEvent : DomainEvent
{
    public OrderPlacedEvent(Guid orderId, Guid customerId, decimal totalAmount)
    {
        OrderId = orderId;
        CustomerId = customerId;
        TotalAmount = totalAmount;
    }

    public Guid OrderId { get; }
    public Guid CustomerId { get; }
    public decimal TotalAmount { get; }
}

Domain Services

Domain Services encapsulate domain logic that doesn’t naturally belong to an entity or value object. Module: Intent.DomainServices
public interface IProductCategoryService
{
    void AssignProductToCategory(Product product, Category category);
    void RemoveProductFromCategory(Product product, Category category);
    bool CanAssignToCategory(Product product, Category category);
}
When to Use Domain Services:
  • Logic spans multiple aggregates
  • Operation doesn’t naturally belong to one entity
  • Behavior requires coordination between domain objects
  • Stateless operations on domain concepts

Modeling in Intent Architect

The Domain Designer

Intent Architect’s Domain Designer is your primary tool for DDD modeling:
1

Create Domain Package

Organize your domain into bounded contexts using packages
2

Model Entities

Create classes representing entities with:
  • Attributes (properties)
  • Operations (methods)
  • Associations (relationships)
3

Define Aggregates

Use composition (black diamond) for owned entities:
  • Aggregate roots are independent entities
  • Owned entities are part of the aggregate
4

Add Value Objects

Create value objects for domain concepts without identity
5

Model Domain Services

Create domain services for cross-aggregate operations

Entity Modeling Patterns

An entity with no incoming associations (or only aggregational ones):
Order
├── Id: Guid
├── OrderNumber: string
└── Items: OrderItem[] (composition)
Generated Code:
  • Full entity class
  • Repository interface and implementation
  • EF Core configuration

Bounded Contexts

Bounded Contexts define clear boundaries within which a domain model applies.

Multiple Contexts in Intent Architect

Solution/
├── Sales Context/              # Bounded Context 1
│   ├── Domain/
│   │   ├── Order              # Sales perspective of Order
│   │   ├── Customer           # Sales perspective of Customer
│   │   └── Product
│   └── Application/
├── Inventory Context/          # Bounded Context 2
│   ├── Domain/
│   │   ├── Product            # Inventory perspective of Product
│   │   ├── Stock
│   │   └── Warehouse
│   └── Application/
└── Shipping Context/           # Bounded Context 3
    ├── Domain/
    │   ├── Shipment
    │   ├── Order              # Shipping perspective of Order
    │   └── Address
    └── Application/
The same business concept (like “Product”) can exist in multiple bounded contexts with different attributes and behaviors relevant to each context.

Context Integration

Bounded contexts communicate through:
  1. Domain Events - Asynchronous integration
  2. Anti-Corruption Layer - Translate between contexts
  3. Shared Kernel - Common domain concepts
// Sales Context publishes event
public class OrderPlacedEvent : DomainEvent
{
    public Guid OrderId { get; set; }
    public List<Guid> ProductIds { get; set; }
}

// Inventory Context subscribes
public class OrderPlacedEventHandler : INotificationHandler<OrderPlacedEvent>
{
    private readonly IInventoryService _inventoryService;

    public async Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
    {
        // Reserve inventory for ordered products
        await _inventoryService.ReserveStockAsync(
            notification.ProductIds, 
            cancellationToken);
    }
}

Ubiquitous Language

DDD emphasizes creating a shared language between developers and domain experts.

Implementing Ubiquitous Language

Name classes, methods, and properties using exact business terminology:
// Good - Uses business language
public class Order
{
    public void Place() { }
    public void Ship() { }
    public void Cancel() { }
}

// Bad - Technical language
public class OrderEntity
{
    public void SetStatus(int status) { }
}
Structure code to reflect actual business workflows:
// Business process: Order Fulfillment
order.Confirm();           // Business approves order
await payment.Process();   // Payment is collected
order.MarkAsShipped();     // Warehouse ships items
order.Complete();          // Customer receives delivery
Don’t hide business concepts behind generic patterns:
// Good - Clear business intent
public interface ICustomerRepository
{
    Task<Customer> FindByEmailAsync(string email);
    Task<List<Customer>> FindPremiumCustomersAsync();
}

// Less clear - Generic abstraction
public interface IRepository<T>
{
    Task<T> FindByPropertyAsync(string property, object value);
}

Entities vs DTOs

A critical distinction in DDD is separating domain entities from data transfer objects:
Purpose: Encapsulate business logic and rules
public class Customer  // Domain Entity
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public CustomerStatus Status { get; private set; }
    
    private readonly List<Order> _orders = new();
    public IReadOnlyCollection<Order> Orders => _orders.AsReadOnly();

    // Business behavior
    public void PlaceOrder(Order order)
    {
        if (Status == CustomerStatus.Suspended)
            throw new InvalidOperationException(
                "Suspended customers cannot place orders");
        
        _orders.Add(order);
    }

    public void Suspend(string reason)
    {
        Status = CustomerStatus.Suspended;
        // Could raise CustomerSuspendedEvent
    }
}
Characteristics:
  • Contains business logic
  • Enforces invariants
  • Has identity and lifecycle
  • Never exposed directly through APIs
Never expose domain entities through APIs. Always map to DTOs. This protects your domain model from API changes and prevents over-posting vulnerabilities.

Mapping Between Entities and DTOs

// Using AutoMapper (Intent.Application.Dtos.AutoMapper)
public class GetCustomerByIdQueryHandler : IRequestHandler<GetCustomerByIdQuery, CustomerDto>
{
    private readonly ICustomerRepository _repository;
    private readonly IMapper _mapper;

    public async Task<CustomerDto> Handle(
        GetCustomerByIdQuery request,
        CancellationToken cancellationToken)
    {
        var customer = await _repository.FindByIdAsync(request.Id, cancellationToken);
        
        // Map entity to DTO
        return customer.MapToCustomerDto(_mapper);
    }
}

Persistence Patterns

Aggregate Persistence

Aggregates are saved as a unit:
public class OrderRepository : RepositoryBase<Order, Order, ApplicationDbContext>, IOrderRepository
{
    public OrderRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    // Saving the aggregate root saves all owned entities
    public void Add(Order order)
    {
        // EF Core tracks the entire aggregate graph
        base.Add(order);
    }
}

Entity Framework Configuration

Intent Architect generates EF Core configurations that respect DDD patterns:
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(x => x.Id);
        
        // Owned entities are part of the aggregate
        builder.OwnsMany(x => x.Items, ConfigureOrderItems);
        
        // Domain events are not persisted
        builder.Ignore(e => e.DomainEvents);
    }

    private void ConfigureOrderItems(OwnedNavigationBuilder<Order, OrderItem> builder)
    {
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Quantity).IsRequired();
        builder.Property(x => x.UnitPrice).IsRequired();
    }
}

Best Practices

Begin design with domain experts, not database tables:
  • Model business concepts first
  • Add persistence concerns later
  • Let the domain drive the database schema
Large aggregates cause performance and concurrency issues:
  • Include only what must be consistent
  • Use domain events for eventual consistency
  • Reference other aggregates by ID only
All business logic should be in the domain layer:
  • Private setters on properties
  • Public methods for state changes
  • Validate invariants in constructors and methods
Share vocabulary between code and conversations:
  • Class and method names match business terms
  • Avoid technical jargon in domain layer
  • Update both code and language when learning occurs

Entities

Intent.Entities - Generate domain entities with rich behavior

Value Objects

Intent.ValueObjects - Immutable value objects

Domain Events

Intent.DomainEvents - Event-driven domain logic

Domain Services

Intent.DomainServices - Cross-aggregate operations

EF Core

Intent.EntityFrameworkCore - DDD-friendly persistence

Repositories

Intent.EntityFrameworkCore.Repositories - Aggregate repositories

Additional Resources

Build docs developers (and LLMs) love