Loading learning content...
When Eric Evans introduced Domain-Driven Design, he presented two complementary sets of patterns that address complexity at different scales. These are often called Strategic DDD and Tactical DDD—and understanding the distinction between them is essential for applying DDD effectively.
Think of it this way: if you're building a city, Strategic Design is urban planning—deciding where neighborhoods go, how districts are organized, where the boundaries are, and how traffic flows between areas. Tactical Design is architecture—designing individual buildings, their internal layouts, structural integrity, and aesthetic coherence.
Both are essential. A city with beautiful individual buildings but no urban plan becomes chaotic. A city with perfect urban planning but poorly designed buildings is unlivable. The same is true for software systems.
By the end of this page, you will understand the difference between Strategic and Tactical DDD, know the key patterns in each category, and grasp how they work together. You'll learn to recognize when each type of pattern is needed and avoid the common mistake of applying tactical patterns without strategic thinking.
Strategic DDD operates at the level of the entire system or organization. It answers questions like:
Strategic DDD recognizes a fundamental truth: not all parts of a system are equal. Some areas are core to competitive advantage and deserve careful modeling. Others are commodities that can be minimally invested in or outsourced. And trying to maintain a single, unified model across a large enterprise is not just impractical—it's actively harmful.
The Big Picture Problem:
In large organizations, different departments use the same words to mean different things. 'Customer' means something different to Sales, Support, and Finance. 'Order' has different attributes and behaviors depending on whether you're in Fulfillment or Accounting. Trying to create a single, enterprise-wide 'Customer' or 'Order' model creates a Frankenstein entity that serves no one well.
Strategic DDD provides the tools to draw boundaries, acknowledge these differences, and design explicit integration points.
The Bounded Context is arguably the most important pattern in all of DDD. It defines the boundary within which a particular model is valid and consistent. Inside a Bounded Context, terms have precise meanings and the model has integrity. Outside that boundary, the same terms may mean something entirely different.
Why Bounded Contexts Matter:
Consider the word 'Product' in an e-commerce company:
Attempting to create a single Product class that serves all these needs creates a monstrosity—a bloated entity with dozens of fields, complex conditional logic, and no clear ownership. Every team's changes risk breaking another team's functionality.
Bounded Contexts solve this by acknowledging that it's perfectly fine—even desirable—to have different models. Each context owns its view of 'Product' and is free to evolve it independently. Integration happens through explicit boundaries, not shared code.
12345678910111213141516171819202122232425262728293031323334353637383940
// === CATALOG BOUNDED CONTEXT ===// Product in the Catalog context focuses on presentation and discovery namespace CatalogContext { // Product as understood by the Catalog team export class Product { constructor( public readonly id: ProductId, public readonly name: LocalizedText, public readonly description: LocalizedText, public readonly category: Category, public readonly specifications: ProductSpecifications, public readonly images: ProductImage[], public readonly seoMetadata: SeoMetadata ) {} // Catalog-specific behavior isVisible(): boolean { return this.images.length > 0 && this.description.hasContent() && this.category.isActive(); } generateSlug(): string { return slugify(this.name.defaultText); } // No inventory concerns here! // No pricing logic here! // Those are separate contexts } export class ProductSpecifications { constructor( public readonly attributes: Map<string, string>, public readonly features: string[], public readonly technicalDetails: TechnicalDetail[] ) {} }}A common misconception is that each Bounded Context must be a separate microservice. While a microservice SHOULD align with a Bounded Context, a Bounded Context can be implemented as a module within a monolith. The logical boundary is what matters—deployment topology is a separate decision. Many successful DDD implementations use a 'modular monolith' where Bounded Contexts are separate modules but deploy together.
Tactical DDD operates within a single Bounded Context. It provides the building blocks for expressing the domain model in code. Where Strategic DDD asks 'How do we organize the overall system?', Tactical DDD asks 'How do we structure the code within each part?'
The Tactical patterns are what most developers encounter first when learning DDD—Entities, Value Objects, Aggregates, Repositories. These patterns have become so widely known that many developers mistakenly think they ARE DDD. But without the Strategic context, applying Tactical patterns becomes a mechanical exercise that misses the point.
Tactical patterns answer questions like:
| Pattern | Purpose | Key Characteristic | Example |
|---|---|---|---|
| Entity | Objects with identity | Identity persists across state changes | Customer, Order, Account |
| Value Object | Objects defined by attributes | Immutable, no identity, equality by value | Money, Address, DateRange |
| Aggregate | Consistency boundary | Group of objects with a single root | Order (with OrderLines) |
| Aggregate Root | Entry point to aggregate | Only external access point | Order is root; OrderLine accessed via Order |
| Repository | Persistence abstraction | Collection-like interface for aggregates | OrderRepository.findById() |
| Domain Service | Stateless domain operations | Operations not belonging to any entity | TransferService, PricingService |
| Domain Event | Capture significant occurrences | Record of something that happened | OrderPlaced, PaymentReceived |
| Factory | Complex object creation | Encapsulate creation logic | OrderFactory.createFromQuote() |
The distinction between Entities and Value Objects is one of the most important concepts in Tactical DDD. Getting this right shapes the entire structure of your domain model.
Entities are objects whose identity matters. Two entities with identical attributes are still different entities if they have different identities. A Customer with ID 12345 is not the same as a Customer with ID 67890, even if they currently have the same name and address.
Value Objects are objects defined entirely by their attributes. Two value objects with identical attributes ARE the same thing. A Money object representing $100 USD is identical to any other Money object representing $100 USD. Making a 'copy' is meaningless—there's nothing to copy that creates something different.
The Decision Heuristic:
Ask yourself: 'If I swap this object with another object having identical attributes, does anything break?'
Why This Matters:
Value Objects should be immutable. Instead of changing a value object, you create a new one. This simplifies threading, improves testability, prevents aliasing bugs, and makes reasoning about your code easier.
Entities can be mutable (their state changes over time), but their identity never changes. The lifecycle of an entity is a significant domain concept.
12345678910111213141516171819202122232425262728293031323334
// ENTITY: Identity matters// Two customers with same name are DIFFERENT class Customer { private readonly id: CustomerId; // Identity private name: string; // Can change private email: Email; // Can change private status: CustomerStatus; // Can change // Equality based on identity equals(other: Customer): boolean { return this.id.equals(other.id); } // State changes are valid operations updateEmail(newEmail: Email): void { this.validateEmailChange(newEmail); this.email = newEmail; this.raise(new CustomerEmailChanged( this.id, newEmail )); } // Lifecycle events are meaningful deactivate(reason: string): void { this.status = CustomerStatus.Inactive; this.raise(new CustomerDeactivated( this.id, reason )); }} // Customer lifecycle: Created → Active → Inactive// The SAME customer moves through states123456789101112131415161718192021222324252627282930313233343536373839404142
// VALUE OBJECT: Attributes define it// Two Money objects of same amount ARE equal class Money { private readonly amount: number; private readonly currency: Currency; // Private constructor - use static factory private constructor(amount: number, currency: Currency) { this.amount = amount; this.currency = currency; } // Static factory methods static of(amount: number, currency: Currency): Money { return new Money(amount, currency); } // Equality based on ALL attributes equals(other: Money): boolean { return this.amount === other.amount && this.currency.equals(other.currency); } // Operations return NEW objects (immutable) add(other: Money): Money { this.assertSameCurrency(other); return new Money( this.amount + other.amount, this.currency ); } multiply(factor: number): Money { return new Money( this.amount * factor, this.currency ); }} // No "money lifecycle" - just valuesIn well-designed domain models, Value Objects often outnumber Entities significantly. Many things that developers instinctively model as Entities are actually Value Objects. Challenge every Entity: 'Does this REALLY need identity?' If not, make it a Value Object and enjoy the benefits of immutability.
Strategic and Tactical DDD are not alternatives—they're complementary. Strategic DDD defines the playing field; Tactical DDD defines how the game is played.
The Relationship:
Strategic First: Before applying tactical patterns, you need to understand the bounded contexts. What model are you building? What's its scope? What are its boundaries?
Tactical Within Context: Each bounded context has its own domain model, built using tactical patterns appropriate to that context's needs.
Strategic Across Contexts: Integration between contexts is handled by strategic patterns (ACL, Shared Kernel, etc.), not by sharing tactical implementations.
Common Anti-Pattern: Tactical Without Strategic:
Many developers learn about Entities, Aggregates, and Repositories and start applying them without strategic thinking. The result is often:
Without bounded contexts to contain them, tactical patterns sprawl across the entire codebase, creating the very complexity DDD was meant to prevent.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
// STRATEGIC: Two Bounded Contexts with different models // === SALES CONTEXT ===// Uses tactical patterns internallynamespace Sales { // Entity within Sales context export class Customer { constructor( private readonly id: CustomerId, private name: string, private creditLimit: Money, private readonly orders: Order[] ) {} placeOrder(items: OrderItem[]): Order { const orderValue = this.calculateOrderValue(items); if (orderValue.exceeds(this.creditLimit)) { throw new CreditLimitExceeded(this.id, this.creditLimit); } const order = Order.create(this.id, items); this.orders.push(order); return order; } } // Aggregate in Sales context export class Order { constructor( private readonly id: OrderId, private readonly customerId: CustomerId, private items: OrderItem[], private status: OrderStatus ) {} }} // === SHIPPING CONTEXT ===// Completely separate model, separate tactical patternsnamespace Shipping { // "Customer" doesn't exist here - we have Recipients export class Recipient { constructor( private readonly externalCustomerId: string, // Reference only private readonly name: string, private readonly shippingAddress: Address ) {} } // Order is a Shipment here - different aggregate export class Shipment { constructor( private readonly id: ShipmentId, private readonly externalOrderId: string, // Reference only private readonly recipient: Recipient, private packages: Package[], private status: ShipmentStatus ) {} // Shipping-specific behavior assignCarrier(carrier: Carrier): void { this.packages.forEach(pkg => pkg.setCarrier(carrier)); this.status = ShipmentStatus.InTransit; } }} // === INTEGRATION via Anti-Corruption Layer ===// Sales context publishes OrderPlaced event// Shipping context subscribes and translates class ShippingOrderHandler { constructor( private readonly recipientLookup: RecipientLookup, private readonly shipmentFactory: ShipmentFactory ) {} async handle(event: OrderPlacedEvent): Promise<void> { // Translate from Sales concepts to Shipping concepts const recipient = await this.recipientLookup.findByCustomerId( event.customerId // Sales term ); const shipment = this.shipmentFactory.createFromOrderEvent( event, recipient ); await this.shipmentRepository.save(shipment); }}A common mistake is creating a 'shared domain model' that multiple contexts depend on. This creates tight coupling and defeats the purpose of bounded contexts. Each context should have its own model. Integration happens through events, APIs, and translation layers—not through shared classes.
Not every part of your system deserves the same level of DDD investment. Strategic DDD helps you decide where to apply intensive tactical modeling, and where to keep things simple.
Subdomain Classification:
DDD classifies subdomains into three categories that guide investment decisions:
Core Domain: This is where your company gains competitive advantage. It's what makes you unique. This is where you invest heavily in tactical DDD—rich models, careful aggregates, sophisticated domain logic.
Supporting Subdomain: Necessary for the business but not a differentiator. A custom solution is needed but doesn't require the sophistication of the core domain. Simpler tactical patterns may suffice.
Generic Subdomain: Commodity functionality that any business needs. Authentication, email sending, payment processing. Often better to buy or use standard solutions rather than build custom models.
| Subdomain Type | Business Value | Uniqueness | DDD Investment | Typical Approach |
|---|---|---|---|---|
| Core | High - competitive advantage | Unique to your business | Maximum - full tactical DDD | In-house, expert team |
| Supporting | Medium - enables core | Somewhat custom | Moderate - simplified DDD | In-house, solid team |
| Generic | Low - commodity | Not unique | Minimal - CRUD is fine | Buy/open-source/outsource |
Example: E-Commerce Platform
Core Domain: Personalized recommendation engine, dynamic pricing, loyalty program
Supporting Subdomain: Product catalog, order management, customer profiles
Generic Subdomain: Authentication, payment processing, shipping label generation
This classification prevents the common mistake of gold-plating everything or treating everything as a nail to DDD's hammer.
In most systems, 80% of the value comes from 20% of the code. That 20% is likely your Core Domain. Strategic DDD helps you identify it so you can invest your best thinking where it matters most, rather than spreading effort uniformly across code that doesn't deserve it.
Understanding what NOT to do is as important as knowing what to do. Here are the most common mistakes teams make when applying Strategic and Tactical DDD:
Strategic and Tactical DDD work together toward a single goal: building software that truly serves the business domain. Let's consolidate the key insights:
What's Next:
We've seen the two dimensions of DDD. But how do teams communicate effectively across these contexts? How do developers and domain experts share a common vocabulary? The answer lies in the Ubiquitous Language—the subject of our next page.
You now understand the distinction between Strategic and Tactical DDD, the key patterns in each category, and how they work together. This framework will guide your DDD journey as you learn to partition systems wisely and model domains richly. Next, we explore the Ubiquitous Language—the glue that holds DDD together.