Domain Driven Design
Why Use DDD?
Imagine the world has collapsed: no roads, no electronics, no factories. You and a handful of survivors want to rebuild some semblance of society. Your first observations:
Overwhelming Complexity
You need water, food, shelter, security, tools, governance… the list goes on.
If you try to “solve everything at once,” you’ll never get past basic survival.
Necessity of Collaboration
No single person can fell enough trees, dig enough wells, or coordinate enough people to rebuild all supply chains.
Miscommunication (calling a “wood bundle” by different names) will lead to wasted effort or dangerous mistakes.
Need for Shared Vocabulary
If one group says “we’ll build a shelter by the north river,” but another thinks “north river” is a different stream, someone ends up building in the wrong place.
Importance of Iteration & Checkpoints
You might start digging a well, then realize you need to refine your design based on water-table depth or geology.
You don’t want to re-start from scratch every time, so you build small, gather feedback, and adjust.
These pressures: complexity, collaboration, evolving requirements... mirror modern software projects. Domain-Driven Design offers a toolkit of strategic and tactical patterns to tame complexity, enforce clear boundaries, and ensure your code mirrors real-world needs.
Strategic DDD: Organizing the Big Picture
Identify Subdomains and the Core Domain
Subdomains are high-level slices of your overall problem space. In our analogy, you might define:
Water Management Subdomain (finding, purifying, distributing water)
Food & Agriculture Subdomain (planting, harvesting, storing crops)
Shelter & Construction Subdomain (building huts, managing materials)
Trade & Governance Subdomain (barter systems, rules, leadership council)
Among these, pick your Core Domain: the area where you must excel to survive or to give your new society an edge.
Example: Maybe in this region, water is scarce. So Water Management is your Core Domain; everything else supports it.
Define Bounded Contexts
Within each subdomain, you create a Bounded Context: a boundary inside which a particular model and its terminology are valid.
In Water Management, everyone agrees on:
Well, Purity, Pipeline, Pump, Distribution Schedule
In Shelter & Construction, the words are:
Foundation, Roof Beam, Insulation, Thermal Vent
Because each context has its own terms, you avoid confusion. If both contexts use the word “Pump,” you might rename one “WaterPump” and the other “AirPump” so the same term doesn’t mean two different things.
Create a Context Map
A Context Map shows how different bounded contexts relate. For example:
Water Management
publishes
a WaterLevelLow
event
Food & Agriculture
listens
for WaterLevelLow
so farmers know to ration irrigation
Shelter & Construction
might react
to RainfallForecast
from a Weather
Subdomain
By making these integrations explicit (shared events, published language), each team knows exactly how to collaborate without merging their internal models.
Tactical DDD: Building the Model Inside a Bounded Context
Once you’ve scoped a Bounded Context like Water Management
, you use these tactical building blocks:
Entities vs. Value Objects
Entity: Has a unique identity that persists over time.
In our well example:
Value Object: Defined solely by its attributes, no distinct identity.
Coordinates (coule be a latitude/longitude pair) and PurityLevel (maybe a percentage) can be Value Objects. They’re immutable and interchangeable if values match.
Aggregates and Aggregate Roots
An Aggregate is a cluster of related objects treated as a single unit for data changes.
For Well
, the Aggregate might include:
Well
: the Aggregate Root
OwnershipHistory
: a small list of past owners
MaintenanceLog
: records of cleaning or repairs
Only the Aggregate Root Well
is loaded/persisted directly; all changes to OwnershipHistory
or MaintenanceLog
go through Well
.
Repositories
A Repository is the mechanism for retrieving and storing Aggregates. In code, you’d define an interface:
Concrete implementations, for example, persisting to a simple file or to an in-memory cache live outside the domain model. The domain code deals only with the interface.
Domain Services
Sometimes a concept doesn’t belong to any single Entity or Value Object. A Domain Service encapsulates domain logic that spans multiple aggregates or doesn’t fit naturally on one.
Example: Calculating “Projected Daily Water Output” might read data from multiple wells and return a combined forecast. That logic lives in a WaterOutput
service, not on any single Well
entity.
Domain Events
When something important happens in the domain, publish a Domain Event. Other parts of the system can subscribe to these events.
Example:
When Well.changeOwner(...)
fires this event, other bounded contexts like Trade & Governance
can listen and update property records or taxation rules.
Putting It All Together: A Mini Walkthrough
Scoping and Language
You convene a team meeting (Event Storming). Survivors from different trades (woodcutters, diggers, farmers) map out all activities on sticky notes.
You discover they all use Well
differently. Water team: “Well with rope pump,” farmers: “Irrigation source,” alkali miners: “Saltwater spring.”
You decide: In Water Management
, a Well
is only a freshwater source
. Somewhere else, use IrrigationWell
or Spring
.
Modeling a Simple Aggregate
Code an entity Well
with fields:
-
WellId
: id -
Coordinates
: location (Value Object) -
PurityLevel
: purity (Value Object) -
Owner
: owner (Value Object or separate Owner entity)
Define business rules:
The Well
repository hides whether you store data in a carved-in-mud ledger or a solar-powered edge server.
Defining Interactions via Context Map
Water Management
: publishes WaterPurityLow
, WaterOutputForecasted
, ...
Food & Agriculture
: subscribes to WaterPurityLow
so that farmers automatically reduce irrigation when purity dips, avoiding crop contamination.
Trade & Governance
: subscribes to WellOwnershipTransferred
so that the community council reassigns water taxes or protection duties.
Iterating & Refining
After a month, you realize some wells freeze in winter. You add a SeasonalTemperature
Value Object and change Well to track oven-powered “thermal wrap” status.
Because your Bounded Context keeps code aligned with real domain events (freezing, thaw, pump failure), you simply add new fields/methods to the Well
aggregate instead of rewriting everything.
Key Takeaways
Domain-Driven Design helps you tame complexity by enforcing:
Strategic patterns (Subdomains, Bounded Contexts, Context Mapping)
Tactical patterns (Entities, Value Objects, Aggregates, Repositories, Domain Events)
Ubiquitous Language is central. Whenever survivors (domain experts) say “Well,” developers should know exactly which definition they mean and code should reflect that single definition.
Bounded Contexts keep teams from stepping on each other’s toes. Each context has its own model and terminology, yet communicates through well-defined interfaces (like published events).
Aggregates and Repositories decouple your domain logic from how data is persisted, so if you switch from carved-stone tablets to microfilm storage, your Well code stays virtually the same.
By building small, iterating, and constantly validating your models against real-world constraints (“Can a rope actually lift this amount of water?”), you reduce rework and ensure the codebase remains aligned with survivors’ needs.