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:

public class Well {
  private WellId id;                   // unique identity
  private Coordinates location;        // Value Object
  private PurityLevel purity;          // Value Object
  private Owner owner;                 // Entity reference or Value Object
  // …methods omitted…
}

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:

public interface WellRepository {
  Well findById(WellId id);
  void save(Well well);
}

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:

public class WellOwnershipTransferred implements DomainEvent {
  private WellId wellId;
  private Owner newOwner;
  private LocalDateTime occurredOn;
  // …constructor/getters…
}

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:

public void transferOwnership(Owner newOwner) {
  if (this.purity.isAboveThreshold()) {
    this.owner = newOwner;
    publishEvent(new WellOwnershipTransferred(id, newOwner));
  } else {
    throw new DomainException("Cannot transfer impure well");
  }
}

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.