Mastering DDD: Layered Architecture, Context Integration, and CQRS Explained
This article walks through the essential concepts of Domain‑Driven Design, detailing a layered architecture, practical code organization, integration techniques such as Open Host Service and Anticorruption Layers, and a concise introduction to CQRS for handling complex queries efficiently.
DDD Layered Architecture
Domain‑Driven Design (DDD) recommends a concentric layering where the Domain layer (rich domain model, aggregates, value objects, domain services) sits at the core and does not depend on any technical concerns. The surrounding layers are:
User Interface (UI) layer : receives external requests, performs validation, logging and response formatting. It contains no business logic.
Application layer : orchestrates use‑cases, coordinates domain services and manages transactions. It only calls domain APIs; it does not implement domain rules.
Domain layer : core business logic expressed with rich domain models. It must be free of infrastructure dependencies.
Infrastructure layer : technical implementations such as persistence, caching, messaging, external adapters. It exposes interfaces that the upper layers depend on.
To avoid circular dependencies the domain layer defines abstractions (e.g., Repository interfaces). Both the domain and infrastructure layers depend on these shared contracts, allowing the infrastructure to provide concrete implementations while keeping the domain pure.
Code Package Layout for a Bounded Context
A typical Go (or Java) project follows the layered structure with a clear package hierarchy:
src/
├─ application // Application services, DTOs, assemblers
├─ domain // Entities, value objects, aggregates, repository interfaces
├─ handler // UI adapters (controllers, gRPC handlers) – technically part of infra
├─ infra // Persistence, messaging, external client implementations
└─ interfaces // Abstract definitions for middleware (e.g., RPC client interfaces)Key responsibilities:
application : converts domain objects to DTOs (assemblers) and defines use‑case services.
domain : contains business rules and repository contracts.
handler : only calls application services; it never accesses domain objects directly.
infra : implements persistence (e.g., JPA, MyBatis), messaging (Kafka, RabbitMQ) and external RPC clients.
interfaces : isolates the domain from concrete middleware by providing only the required method signatures.
Integration Between Bounded Contexts
Two primary integration styles are used in DDD:
RPC‑based integration – defined by an Open Host Service (OHS) and a Published Language (PL) . The service provider publishes an IDL (e.g., Thrift, protobuf) that downstream contexts can call.
Domain‑event integration – events are published on a message bus and consumed by interested contexts.
Example Thrift IDL for a GetProductDetail service:
service ProductService {
ProductDetail GetProductDetail(1: i64 productId);
}
struct ProductDetail {
1: i64 id,
2: string name,
3: string imageUrl,
4: double price,
// ... other fields
}When a downstream context needs only a subset of the data, an Anticorruption Layer (ACL) isolates it from upstream changes. The ACL consists of:
Adapter : invokes the upstream RPC client.
Translator : maps the upstream DTO to the local domain model (or a simplified view object).
Typical directory for the ACL:
src/
├─ interfaces/
│ └─ product_client.go // RPC client interface
├─ infra/
│ └─ product_client_impl.go // concrete Thrift/GRPC client
└─ application/
└─ product_acl.go // adapter + translator logicThe ACL can also host cross‑cutting concerns such as caching, circuit‑breaker logic, or feature‑toggle handling.
CQRS (Command‑Query Responsibility Segregation) Simple Implementation
CQRS separates write operations (commands) from read operations (queries). Three common query strategies are:
Reuse the domain model : simple queries read directly from aggregates via repositories.
Use a data model : for pagination or multi‑aggregate queries, execute raw SQL or lightweight DTO queries that fetch only required columns.
Separate read model : a dedicated query service (often backed by a read‑optimized store such as Elasticsearch) consumes domain events to build a denormalized view.
Example 1 – Reusing the domain model
// Application service method
func (s *InventoryQueryService) GetStock(productID string) (*StockView, error) {
agg, err := s.repo.FindByID(productID)
if err != nil { return nil, err }
return &StockView{ID: agg.ID, Qty: agg.Quantity}, nil
}Example 2 – Data model query
// Direct SQL for a lightweight view
rows, err := db.Query(`SELECT id, name, price FROM product WHERE id = ?`, id)
// map rows to ProductSummary DTOExample 3 – Separate read model
// Domain event publisher (in the write side)
func (o *Order) Complete() {
event := OrderCreated{OrderID: o.ID, Items: o.Items}
o.eventBus.Publish(event)
}
// Read side listener builds an Elasticsearch document
func (h *OrderEventHandler) Handle(event OrderCreated) {
doc := map[string]interface{}{ "orderId": event.OrderID, "items": event.Items }
es.Index("orders", doc)
}Queries then hit Elasticsearch directly, achieving sub‑second response times for complex reporting scenarios.
Supporting Concepts
Repository pattern : domain layer defines Repository interfaces; infrastructure provides concrete persistence implementations.
Domain services : encapsulate behavior that does not naturally belong to a single entity; they are named with verbs to avoid misuse.
Factories : create aggregates or value objects while keeping construction logic out of the domain model.
Application services : expose use‑case APIs to the UI layer, orchestrating domain services and handling transactions.
Domain events : enable eventual consistency across bounded contexts and drive read‑model updates.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
