Stackbook Logo
← Back to Tracks

Domain-Driven Design: An Introduction Track

A guided introduction to strategic and tactical domain-driven design: ubiquitous language, bounded contexts, aggregates, and exercises to apply them.

5 min read

Domain-driven design, introduced by Eric Evans in his 2003 book, is an approach to building software for complex business domains by centring the model on the domain itself and the language of the people who know it. It is often reduced to a list of tactical patterns, but its most valuable parts are strategic: how to divide a large problem and how to keep teams and code aligned with the business.

Learning goals

  • Build a shared language between engineers and domain experts.
  • Identify bounded contexts and draw a context map.
  • Use entities, value objects and aggregates to protect invariants.
  • Know when DDD is worth its cost, and when it is not.

Week 1: Ubiquitous language and discovery

The ubiquitous language is a vocabulary shared by developers and domain experts and used in conversation, documentation and code. If the business says "policy lapses" and the code says status = 3, translation errors follow.

Steps:

  1. Interview a domain expert and collect terms.
  2. Run an event storming session: place orange sticky notes for domain events in past tense, such as OrderPlaced, PaymentFailed, along a timeline. Then add commands that cause them, and the actors.
  3. Note disagreements. When two people use one word for different things, you have found a context boundary.

Exercise: hold a 90-minute event storm for one workflow you know, and produce a glossary of 15 terms with definitions.

Week 2: Strategic design

A bounded context is a boundary within which a model and its language are consistent. "Customer" means something different to sales, billing and support; forcing one model across all of them creates an unmaintainable compromise.

A context map shows relationships between contexts:

[Sales] --customer/supplier--> [Billing]
[Billing] --anticorruption layer--> [Legacy ERP]
[Shipping] --conformist--> [Carrier API]

Common relationships include shared kernel, customer-supplier, conformist, and anticorruption layer, which translates between models to keep a foreign model from leaking into yours.

Classify subdomains by importance:

  • Core: where you differentiate; invest your best people and rich modelling.
  • Supporting: necessary but not differentiating; keep it simple.
  • Generic: solved problems such as authentication; buy or use off the shelf.

Exercise: draw a context map for a system you know, labelling each context core, supporting or generic, and mark one relationship that needs an anticorruption layer.

Week 3: Tactical building blocks

Within a bounded context, these patterns express the model:

  • Entity: an object defined by identity that persists over time, such as an Order.
  • Value object: defined by its attributes and immutable, such as Money(amount, currency).
  • Aggregate: a cluster of objects treated as a consistency unit, with one root through which all changes pass.
  • Domain event: a record of something that happened.
  • Repository: an abstraction for loading and saving aggregates.
  • Domain service: behaviour that does not belong to a single entity.

An aggregate protects an invariant. For example, an order's total must equal the sum of its lines:

class Order:
    def __init__(self, order_id):
        self.id = order_id
        self._lines = []

    def add_line(self, sku, qty, unit_price):
        if qty <= 0:
            raise ValueError("quantity must be positive")
        self._lines.append((sku, qty, unit_price))

    def total(self):
        return sum(q * p for _, q, p in self._lines)

Keep aggregates small. A common guideline is one transaction per aggregate, with eventual consistency between aggregates via events. Large aggregates cause contention and slow writes.

Exercise: model one aggregate from your domain, listing its invariants and the commands that can change it. Decide which references to other aggregates are by identity only.

Week 4: Connect to architecture

Bounded contexts are natural candidates for module or service boundaries, but they do not require microservices. A well-structured modular monolith with context-aligned modules often delivers most of the benefit with less operational cost. Align teams to contexts to reduce coordination, as Conway's observation suggests.

Exercise: map each bounded context to a module or service, and list the integration events between them.

When DDD is not worth it

DDD pays off where domain logic is complex and changing. For simple CRUD applications, the ceremony adds cost without benefit. Be especially wary of applying tactical patterns, such as elaborate repositories everywhere, to trivial domains.

Common mistakes

  • Skipping the language work and jumping to patterns.
  • One giant model shared across the entire company.
  • Anemic models where all logic lives in services.
  • Treating bounded contexts as microservices by default.

Key takeaways

  • Start with ubiquitous language, built with domain experts through conversation and event storming.
  • Bounded contexts give each model a clear boundary; a context map documents their relationships.
  • Invest modelling effort in the core domain and buy or simplify the rest.
  • Aggregates protect invariants and define transaction boundaries; keep them small.
  • Contexts guide modular boundaries without forcing microservices.
  • Use DDD where domain complexity justifies it.