iTranslated by AI
Drafting Context Maps Before Coding: A Guide to Strategic DDD
Introduction
Aggregates, Value Objects, Repositories, and Use Cases... Introducing tactical DDD patterns has certainly improved code quality. However, as systems span across multiple teams and services, other issues arise:
- The "User" defined by one team is slightly different from the "User" of another, yet the same type is being shared.
- Changes in an external API directly impact your own domain model.
- No one knows which context depends on which.
These are problems that tactical DDD alone cannot solve. This is because they stem from the relationships between teams and contexts, not the structure of the code. In this article, I will introduce the Context Map, a tool for visualizing and resolving these issues.
Problems That Tactical DDD Cannot Solve
Tacitcal DDD focuses on "how to design the inside of a single Bounded Context."
A Bounded Context is the boundary within which a specific domain model has a consistent meaning. For example, a "Product" in an "Order" context and a "Product" in a "Catalog" context may contain different information or behaviors despite sharing the same name. By clarifying these boundaries, each team can evolve the models within their own context freely.
However, in real-world systems, multiple contexts work in collaboration.
If relationships between contexts remain implicit, the following problems occur:
| Issue | Example |
|---|---|
| Model Pollution | The Order context directly uses the Transaction type from the Payment service, causing the domain model to break when the Payment API specs change. |
| Hidden Dependencies | The Inventory context directly references internal types of the Shipping context, making the impact of changes invisible. |
| Team Friction | Lack of consensus on changes to shared libraries leads to development stalls. |
Eric Evans proposed the Context Map as a way to deal with these issues. A Context Map is a tool for defining the relationships between Bounded Contexts. It is not just a diagram; it is intended to record organizational and technical integration patterns (Evans, Domain-Driven Design, 2003, Chapter 14).
How to Draw a Context Map
A Context Map is not just an architectural diagram; it also expresses the power dynamics and collaboration policies between teams.
Step 1: Identify Contexts
First, list the Bounded Contexts that exist in your system.
1. Order Context (Order acceptance and management)
2. Inventory Context (Inventory level management and allocation)
3. Catalog Context (Product information management)
4. Shipping Context (Shipping arrangement and tracking)
5. Payment Context (Integration with external payment services)
6. Notification Context (Sending emails and push notifications)
Step 2: Identify Relationship Patterns
Classify the relationships between each context into the relationship patterns defined in DDD. The main patterns are as follows:
| Pattern | English Name | Overview |
|---|---|---|
| Shared Kernel | Shared Kernel | Two contexts share a portion of the model |
| Customer-Supplier | Customer-Supplier | The upstream (supplier) provides an API according to the downstream (customer) needs |
| Conformist | Conformist | The downstream follows the upstream's model exactly |
| Anti-Corruption Layer | Anti-Corruption Layer | The downstream creates a translation layer to protect itself from the upstream model |
| Open Host Service | Open Host Service | The upstream provides a public protocol (API) |
| Published Language | Published Language | A standard data format shared between contexts |
| Separate Ways | Separate Ways | Contexts do not interact and are developed independently |
Step 3: Draw the Map
Legend: Arrows indicate the flow of data or API calls. Labels with 'U' (Upstream) / 'D' (Downstream) indicate which side has control over the model. '---' (no direction) indicates an equal relationship.
From this map, we can understand the following:
- An ACL is required between Order and the Payment service (to protect ourselves from the external service's model).
- Order and Inventory have a Customer-Supplier relationship (Inventory is upstream=supplier, Order is downstream=customer; the Inventory team adjusts the API according to the needs of the Order team).
- Catalog and Order have a Shared Kernel (sharing product IDs and basic product information).
- In the integration from Shipping to Notification, the Notification side conforms to the Shipping model.
Relationship Patterns Between Teams (Detailed)
Shared Kernel
This pattern involves two contexts sharing a portion of the domain model. Any changes to the shared part require consensus from both teams.
shared/
├── go.mod
├── money.go # Value object for money
├── product_id.go # Type definition for product ID
└── address.go # Value object for address
// shared/money.go
package shared
// Money is a value object for money shared across multiple contexts.
// Changes require agreement between the Order team and the Catalog team.
type Money struct {
Amount int64
Currency string
}
func NewMoney(amount int64, currency string) Money {
return Money{Amount: amount, Currency: currency}
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, ErrCurrencyMismatch // Error definition omitted
}
return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}
Note: While the Shared Kernel is convenient, widening the scope of sharing increases coupling between contexts. Limit sharing to items with low change frequency, such as value objects or ID types.
Evans states that the Shared Kernel is a relationship where two teams share a subset of a small model they have agreed upon. The kernel should be kept small (Evans, Domain-Driven Design, 2003, Chapter 14).
Customer-Supplier
In this pattern, the upstream (supplier) team designs APIs according to the needs of the downstream (customer) team.
// inventory/service.go (Upstream: Supplier)
package inventory
import "context"
// StockService is a service provided by the Inventory context.
// The API is designed according to the needs of the Order context (Customer).
type StockService struct {
repo stockReader
}
// CheckAvailability is an inventory check API required by the Order context.
// It receives a product ID and quantity and returns availability based on customer needs.
func (s *StockService) CheckAvailability(ctx context.Context, productID string, quantity int) (*Availability, error) {
stock, err := s.repo.FindByProductID(ctx, productID)
if err != nil {
return nil, fmt.Errorf("failed to get stock: %w", err)
}
return &Availability{
ProductID: productID,
Available: stock.Quantity >= quantity,
Remaining: stock.Quantity,
}, nil
}
// order/usecase/place_order.go (Downstream: Customer)
package usecase
// StockStatus is a type for inventory check results defined by the Order context.
// It does not directly use types from the inventory package but defines its own.
type StockStatus struct {
ProductID string
Available bool
Remaining int
}
type stockChecker interface {
CheckAvailability(ctx context.Context, productID string, quantity int) (*StockStatus, error)
}
type PlaceOrderUseCase struct {
stock stockChecker
orderRepo orderWriter
}
The downstream context does not directly reference upstream types (inventory.Availability) but defines its own (StockStatus). By converting from upstream types to downstream types in the stockChecker implementation (infrastructure layer adapter), the boundary between contexts is maintained.
In a Customer-Supplier relationship, the downstream team can request APIs that they need from the upstream team, and the upstream team designs the API taking those requests into account.
Conformist
In this pattern, the downstream follows the upstream's model exactly. This is adopted when the upstream team lacks the willingness or capacity to respond to downstream requests, and the cost of building an ACL (translation layer) is higher than the risk of dependency on the upstream model.
// notification/handler.go (Downstream: Conformist)
package notification
import "myapp/shipping"
// HandleShipmentEvent receives the event from the Shipping context as is,
// conforming to the Shipping context's model to send notifications.
type HandleShipmentEvent struct {
sender emailSender
}
func (h *HandleShipmentEvent) Handle(ctx context.Context, event *shipping.ShipmentStatusChanged) error {
// Use the Shipping context's model as is
subject := fmt.Sprintf("Shipping Update: %s", event.TrackingID)
body := fmt.Sprintf("Shipping status changed to '%s'.", event.NewStatus)
return h.sender.Send(ctx, event.CustomerEmail, subject, body)
}
In the Conformist pattern, you weigh the cost of establishing an ACL against the risk of dependency on the upstream model. For auxiliary contexts like notifications, conforming to the upstream model can sometimes be practical.
Steps for Application in Real Projects
Here are concrete steps for introducing a Context Map into a real project.
Step 1: Visualize Current Context Boundaries
Identify implicit context boundaries from the existing codebase.
# Visualize dependency between packages in a Go project
go list -json ./... | jq '.ImportPath'
# Check import relationships between packages
go list -f '{{.ImportPath}}: {{join .Imports ", "}}' ./internal/...
Visualize these dependencies and check for circular or unnatural dependencies.
Step 2: Discuss Boundaries with Domain Experts
Consider not only technical dependencies but also business boundaries:
- "Who is responsible for this concept?"
- "Are these two teams using the same terms with the same meanings?"
- "Can this service be changed independently by another team?"
Step 3: Determine Relationship Patterns
Select the relationship for each context from the patterns described above based on the following criteria:
| Situation | Recommended Pattern |
|---|---|
| Both teams can collaborate closely | Shared Kernel |
| Upstream team can respond to downstream requests | Customer-Supplier |
| Upstream cannot respond and translation cost > dependency risk | Conformist |
| Collaboration with external services or legacy systems | Anti-Corruption Layer |
| No need for collaboration | Separate Ways |
Step 4: Reflect in Code
Reflect the relationship patterns determined in the Context Map in your directory structure and implementation.
myapp/
├── internal/
│ ├── order/ # Order Context
│ │ ├── domain/
│ │ ├── usecase/
│ │ └── infra/
│ │ └── acl/ # ACL for Payment Service
│ │ ├── payment_adapter.go
│ │ └── payment_translator.go
│ ├── inventory/ # Inventory Context (Supplier to Order)
│ ├── catalog/ # Catalog Context
│ ├── shipping/ # Shipping Context
│ └── notification/ # Notification Context (Conformist to Shipping)
├── shared/ # Shared Kernel
│ ├── money.go
│ └── product_id.go
└── docs/
└── context-map.md # Context Map documentation
Step 5: Maintain the Map
Creating a Context Map is not a one-time task. Review it on the following occasions:
- When adding a new context (service).
- When team structures change.
- When starting a new integration with an external service.
- When issues arise with dependencies between contexts.
It is recommended to manage the map as documentation within the repository and update it alongside your ADR (Architecture Decision Record).
Summary
| Aspect | Content |
|---|---|
| Limits of Tactical DDD | Solves design within a single context but does not address relationships between them. |
| Role of Context Map | Visualizes relationships between contexts and clarifies collaboration patterns. |
| Key Relationship Patterns | Shared Kernel, Customer-Supplier, Conformist, Anti-Corruption Layer, etc. |
| Points for Application | Visualize current state first, discuss with domain experts, then reflect in code. |
Writing "good code" with tactical DDD and drawing "correct boundaries" with strategic DDD are like two wheels of a car. By drawing a Context Map, it becomes clear where an ACL is needed, which models should be shared, and which team should be the supplier. I recommend drawing a map before writing code.
References
| Content | Source |
|---|---|
| Original source of Context Map | Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003) |
| Explanation of Strategic DDD patterns | Vaughn Vernon, Implementing Domain-Driven Design (2013) |
| Practical use of Context Map | Martin Fowler, BoundedContext |
| Organizing inter-team patterns | Vladik Khononov, Learning Domain-Driven Design (2021) |
| Go Module structure | Go Modules Reference |
Terminology Notes
Context: Throughout this article, the term "context" refers to the range of premises and scope where a concept holds a specific meaning. DDD explicitly defines these boundaries as "Bounded Contexts" to serve as units of design. For details, refer to the "Problems That Tactical DDD Cannot Solve" section.
Discussion
ステップ2:ドメインエキスパートと境界を議論する
コレを機会として持つのが難しいんよな…
そうなんですよねぇ...