Skip to main content

Strategic Design — Subdomains, Ubiquitous Language, and Context Maps

In the previous chapter we said DDD has two layers: where to draw the lines, and how to build what is inside them. This chapter covers the former.

Drawing lines breaks down into three activities: deciding where in the business to put your effort (subdomains), agreeing on the words the team uses (ubiquitous language), and delimiting the range over which a model holds (bounded contexts). After that, you decide how the delimited ranges connect to each other (context maps).

Dividing the domain — core, supporting, generic

Not every part of a business carries the same weight. DDD divides a domain into subdomains of different character.

KindWhat it isHow to treat it
CoreWhere you differ from competitors. The center that earns the revenueBuild it yourself. Spend the most design time here
SupportingNeeded to run the core, but not a differentiatorBuild it yourself, but not as carefully as the core
GenericTakes the same shape at any companyBuy it. An off-the-shelf product or SaaS

For an e-commerce site:

KindExampleWhy
CoreOrders, inventory reservationHow stock is held and when an order is confirmed drives the competitiveness of the business
SupportingProduct catalogNecessary, but building it carefully rarely sets you apart
GenericAuthentication, payment, mail deliveryLaravel Sanctum, Stripe, and SES are enough

This classification is material for deciding where to spend design effort. Carefully designing value objects and aggregates in a generic subdomain has little effect on the business. Leaning on an existing package for authentication, as Chapter 22 does, leaves more time for the core.

note

Whether something is core is not decided by technical difficulty. Something can be technically hard and still generic (full-text search), or simple and still core (the order in which discounts apply). The axis is "does this set us apart?"

Ubiquitous language

The ubiquitous language is the set of words the whole team shares. When developers, domain experts, and product managers all use the same words, misunderstandings have fewer places to hide.

Why aligning the words matters

When the words are not aligned, a translation step sits between the business and the code.

Words not alignedWords aligned
Business stakeholder"Please confirm the order""Please confirm the order"
Developer"I'll update status to 1 in the orders table""I'll call Order::confirm()"
What happensThe concept of "confirming" disappears from the codeThe word from the business stays in the code

With a translation step, every specification change means redoing the same translation. When you hear "hold the stock at confirmation," working out where in the "set status to 1" routine the stock handling goes is a different amount of work from opening Order::confirm().

When the words of the business match the method names, the code reads as the specification.

The business wordThe code
Confirm an orderOrder::confirm()
Cancel an orderOrder::cancel()
Add a productOrder::addItem()
Calculate the shipping feeOrder::calculateShippingFee()
note

The ubiquitous language is not a glossary of business terms. It is the living language everyone uses day to day. You have to keep using the same words in meetings, in code, and in documents. Build a glossary and stop there, and the glossary drifts away from reality.

Bounded context

Try to align the words and you hit a wall immediately: the same word points at different things depending on the setting.

Martin Fowler gives the example of an electric utility. The word "meter" means the connection point in one department and the physical device in another. Conversation tolerates the ambiguity; a program does not1.

A bounded context is the range over which a particular domain model is valid. Delimit the range and the same word can carry a different model in each context.

The same "product" shifts by setting

Here is "product" in an e-commerce site across three settings.

ContextWhat "product" meansWhat it holds
OrdersThe purchase target on an order lineProduct ID, name, price, quantity
InventoryStock held in a warehouseProduct ID, stock count, warehouse location, expected arrival date
CatalogProduct information shown to customersProduct ID, description, images, category

The only thing in common is the product ID. Fold these three into one Product class and the expected arrival date becomes visible while processing an order, and the stock count becomes visible while rendering the catalog. It stops being clear which attribute carries meaning in which setting, and every change comes with checking whether you broke an unrelated one.

Draw the boundaries and each context holds only what it needs.

It resembles the scope of a department

A bounded context resembles the scope of a department's work.

DepartmentWhat "customer" meansWhat they care about
SalesA business partner who generates revenueContract value, transaction history, account manager
SupportSomeone you provide support toInquiry history, product information, case status

The same word "customer," but each department looks at something different. Showing the inquiry history to sales does not change their decisions, and showing the contract value to support does not change their response.

note

In a small system, handling everything in a single context is fine. Separating contexts is worth considering once the same word starts carrying different meanings. Separate too early and all you get is translation code across a boundary that has no substance.

Context map — how boundaries relate

Once you have boundaries, you decide how they connect. A context map is that set of relationships written down.

Relationships first split into three by the balance between teams2.

Kind of relationshipContent
Mutually dependentNeither context works unless both ship
Upstream / downstreamUpstream changes affect downstream, but not the reverse
FreeChanges on either side do not affect the other

On top of that sit concrete patterns. Five of them are the ones that call for a judgment in practice.

PatternThe relationshipWhen to choose it
PartnershipBoth teams coordinate and ship togetherWhen a failure on one side fails both
Shared kernelAn explicitly shared subset of the modelWhen you want to avoid duplication, at the cost of agreement on every change
Customer / supplierDownstream priorities enter the upstream planWhen there is room to negotiate with upstream
ConformistFollow the upstream model as it isWhen there is no room to negotiate and you want to avoid the complexity of translating
Anticorruption layerInsert a layer that translates the upstream model into your ownWhen you do not want the upstream model inside your design

A closer look at the anticorruption layer

Of the five, the anticorruption layer (ACL) is the one you reach for most often in Laravel.

An external service's model is not built for your domain. A Stripe PaymentIntent has seven states: requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, and succeeded3. Those seven exist for the sake of the payment flow; from the order's side, "done," "not yet," and "failed" are enough.

Carry all seven into the domain layer and the ordering code knows about the payment service's circumstances. Add one state on Stripe's side and you edit the branches in ordering.

An anticorruption layer translates an external model into your own words.

// app/Infrastructure/Payment/StripePaymentGateway.php

// The domain layer only knows PaymentResult.
// The word PaymentIntent never leaves this layer.
final class StripePaymentGateway implements PaymentGatewayInterface
{
public function pay(OrderId $orderId, Money $amount): PaymentResult
{
$intent = $this->stripe->paymentIntents->create([
'amount' => $amount->amount(),
'currency' => strtolower($amount->currency()),
'metadata' => ['order_id' => $orderId->value()],
]);

// Fold the external seven states into the three our domain has.
// The differences among requires_* belong to the payment service;
// to the order they carry the single meaning "not finished yet."
return match ($intent->status) {
'succeeded' => PaymentResult::completed(new PaymentId($intent->id)),
'processing',
'requires_action',
'requires_capture',
'requires_confirmation',
'requires_payment_method' => PaymentResult::pending(new PaymentId($intent->id)),
'canceled' => PaymentResult::failed(
$intent->last_payment_error->message ?? 'The payment was canceled',
),
};
}
}

The domain decides how coarse the folding is. If the design waits for payment to complete before reserving stock, only the distinction between pending and completed matters. If you want to show "this card needs an extra step" on screen, requires_action has to be a state of its own. How many states you fold the external ones into is decided by how many your business needs to tell apart.

With this shape, swapping payment services changes only this class. The domain and use case layers know only PaymentGatewayInterface and PaymentResult, so the word PaymentIntent appears in exactly one place.

Which layer the interface belongs to is the same judgment as the repository in Chapter 17. The contract goes in the domain layer and only the implementation goes in infrastructure.

Drawing the lines in a Laravel project

A bounded context does not have to mean a separate application. You can start by separating directories inside a single Laravel application.

app/Domain/
├── Order/ ← order context
│ ├── Order.php
│ ├── OrderLine.php
│ └── OrderRepositoryInterface.php
├── Inventory/ ← inventory context
│ ├── Inventory.php
│ └── InventoryRepositoryInterface.php
└── Shared/ ← things that mean the same in every context
├── Money.php
└── EmailAddress.php

There is one rule to keep here. When you cross contexts, do not touch the other side's internal classes. If the order context wants to change a stock quantity, it passes an identifier and coordinates in the use case layer rather than operating the Inventory entity directly. The reason is the same as the aggregate discussion in Chapter 10: who guarantees consistency.

Whether to split into separate applications comes down to:

  • Do you want separate deployment units? Wanting to release inventory frequently on its own is a reason to split
  • Are the teams separate? The more teams touch one codebase, the higher the coordination cost
  • Do you need immediate consistency across the data? Split it and you can no longer update both in one transaction, so you design for eventual consistency instead

Not splitting is the safer default. If you can hold the boundaries inside one application, you can always split later. Split while the boundaries are still vague and you get services that stay tightly coupled while the communication gets harder.

Symptoms of a boundary in the wrong place

You can notice a misplaced line from these symptoms.

SymptomWhat is going on
Attributes of one class are null depending on the settingSeveral contexts are expressed with one model
Changing one feature breaks tests for an apparently unrelated oneSomething crosses a boundary and touches internals directly
The team keeps rephrasing the same word ("order," "purchase," "sales order")The ubiquitous language is not aligned. Another context sits behind it
One change needs agreement from several teamsThe shared kernel is too large

You can redraw after the symptoms appear. There is no need to get the line right the first time. The workshop in the next chapter is a technique for laying out the flow of the business and finding the candidates.

Summary

PointContent
SubdomainsSplit into core, supporting, and generic, and put the design effort on the core
Ubiquitous languageAlign the words of the business and the words of the code. A living language, not a glossary
Bounded contextThe range a model holds over. The same word carrying different meanings is the signal to split
Context mapHow boundaries relate. In Laravel, the anticorruption layer comes up most
Splitting in LaravelStart with app/Domain/<Context>/ directories. Splitting the application comes later

In the next chapter, we look at how to find these boundaries.

Further reading

  • Martin Fowler, "BoundedContext" — an overview, and the example of a word shifting meaning by setting
  • ddd-crew/context-mapping — the nine context map patterns and the three kinds of team relationship
  • Eric Evans, Domain-Driven Design, Part IV — the original source for strategic design
  • Vaughn Vernon, Implementing Domain-Driven Design — subdomains and bounded contexts from an implementation angle

Footnotes

  1. Martin Fowler, "BoundedContext"

  2. The pattern names and the classification follow the summary in ddd-crew/context-mapping. The original source is Part IV of Eric Evans's Domain-Driven Design.

  3. The status field in the Stripe API Reference: The PaymentIntent object. Note that the Charges API cannot be used for new payments.