
Domain Driven Design
- 31 installs
- 21 repo stars
- Updated August 5, 2026
- joaquimscosta/arkhe-claude-plugins
Guidance for Domain-Driven Design: bounded contexts, aggregates, context mapping, and modular monolith vs microservices.
About
Covers DDD strategic and tactical design from domain discovery through bounded contexts, architecture selection, and aggregates. A developer uses it when modeling complex business systems.
- Emphasizes strategic design (boundaries, language) over tactical patterns
- When-to-apply vs when-DDD-is-overkill decision guidance
Domain Driven Design by the numbers
- 31 all-time installs (skills.sh)
- Ranked #3,366 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joaquimscosta/arkhe-claude-plugins --skill domain-driven-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 31 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 5, 2026 |
| Repository | joaquimscosta/arkhe-claude-plugins ↗ |
What it does
Guidance for Domain-Driven Design: bounded contexts, aggregates, context mapping, and modular monolith vs microservices.
Files
Domain-Driven Design Skill
DDD manages complexity through alignment between software and business reality. Strategic design (boundaries, language, subdomains) provides more value than tactical patterns (aggregates, repositories).
When to Apply DDD
Apply DDD when:
- Domain has intricate business rules
- System is long-lived and high-value
- Domain experts are available
- Multiple teams/departments involved
- Software represents competitive advantage
DDD is overkill when:
- Simple CRUD applications
- Tight deadlines, limited budgets
- No domain experts available
- Complexity is purely technical, not business
Core Workflow
1. Domain Discovery → 2. Bounded Context Definition → 3. Context Mapping → 4. Architecture Selection → 5. Tactical Implementation
See WORKFLOW.md for detailed step-by-step instructions for each phase.
Quick Reference
Subdomain Types (Problem Space)
| Type | Investment | Example |
|---|---|---|
| Core | Maximum - competitive advantage | Recommendation engine, trading logic |
| Supporting | Custom but quality tradeoffs OK | Inventory management |
| Generic | Buy/outsource | Auth, email, payments |
Key Decision: Entity vs Value Object
- Entity: Has identity, tracked through time, mutable →
Customer,Order - Value Object: Defined by attributes, immutable, interchangeable →
Money,Address,Email
Default to value objects. Only use entities when identity matters.
Aggregate Design Rules (Vaughn Vernon)
1. Model true invariants in consistency boundaries 2. Design small aggregates (~70% should be root + value objects only) 3. Reference other aggregates by ID only 4. Use eventual consistency outside the boundary
Architecture Decision
Start with modular monolith when:
├── Team < 20 developers
├── Domain boundaries unclear
├── Time-to-market critical
└── Strong consistency required
Consider microservices when:
├── Bounded contexts have distinct languages
├── Teams can own full contexts
├── Independent scaling required
└── DevOps maturity existsDetailed References
- Strategic Patterns: See references/STRATEGIC-PATTERNS.md for subdomains, bounded contexts, context mapping, event storming
- Tactical Patterns: See references/TACTICAL-PATTERNS.md for entities, value objects, aggregates, services, repositories
- Architecture Alignment: See references/ARCHITECTURE-ALIGNMENT.md for clean/hexagonal architecture, modular monolith, microservices
- Workflow: See WORKFLOW.md for detailed step-by-step DDD implementation process
- Anti-Patterns: See references/ANTI-PATTERNS.md for common pitfalls and how to avoid them
- Examples: See EXAMPLES.md for scenario walkthroughs applying DDD concepts
- Troubleshooting: See TROUBLESHOOTING.md for common issues and solutions
Critical Reminders
1. Ubiquitous language first - Code should read like business language 2. Strategic before tactical - Understand boundaries before implementing patterns 3. Apply tactical patterns selectively - Only in core domains where complexity warrants 4. One aggregate per transaction - Cross-aggregate consistency via domain events 5. Persistence ignorance - Domain layer has no infrastructure dependencies
Related Skills
| Need | Skill |
|---|---|
| Data layer implementation | spring-boot-data-ddd — JPA/JDBC aggregates, repositories, transactions |
| REST API layer | spring-boot-web-api — Controllers, validation, exception handling |
| Module boundaries | spring-boot-modulith — Module structure, event-driven communication |
| Testing patterns | spring-boot-testing — Aggregate tests, module tests, Scenario API |
| Security for domains | spring-boot-security — Method-level authorization, role-based access |
Domain-Driven Design Examples
Scenario walkthroughs demonstrating how to apply DDD thinking to real-world systems.
Subdomain Identification
An e-commerce company wants to redesign its monolithic platform. The team runs an Event Storming session and identifies these business capabilities:
- Product catalog and search
- Order placement and fulfillment
- Pricing with dynamic rules (loyalty tiers, flash sales, bundle discounts)
- Inventory tracking across warehouses
- Shipping logistics and carrier integration
- Customer accounts and authentication
- Payment processing
- Email/SMS notifications
Analysis
Classify each by strategic importance:
| Subdomain | Type | Reasoning |
|---|---|---|
| Pricing Engine | Core | Dynamic pricing is the competitive advantage — competitors use flat pricing. Custom rules that directly drive revenue. |
| Order Fulfillment | Core | Orchestrates the end-to-end purchase flow with business-specific rules (partial shipments, backorders, split payments). |
| Product Catalog | Supporting | Necessary but not differentiating. Custom enough to need internal development, but quality tradeoffs are acceptable. |
| Inventory | Supporting | Important for operations but standard warehouse tracking. Could use simpler patterns. |
| Shipping | Supporting | Custom integration with carriers, but the logic itself is standard. |
| Authentication | Generic | Use an identity provider (Keycloak, Auth0). No competitive value in building this. |
| Payments | Generic | Use Stripe/Adyen. Payment processing is commoditized. |
| Notifications | Generic | Use a messaging service (SendGrid, Twilio). Standard integration work. |
Key points:
- Invest maximum DDD effort (tactical patterns, domain experts) in Core subdomains
- Supporting subdomains get simpler patterns — possibly CRUD with some domain logic
- Generic subdomains are bought or outsourced, never built from scratch
---
Bounded Context Definition
The team notices that the word "Order" means different things to different departments:
- Sales talks about Orders as shopping carts with line items, discounts, and customer preferences
- Shipping talks about Orders as packages with weight, dimensions, destination address, and carrier assignment
- Accounting talks about Orders as invoices with tax calculations, payment status, and revenue recognition dates
Analysis
Each department has its own ubiquitous language for "Order." This is the signal to draw bounded context boundaries:
Sales Context Shipping Context Accounting Context
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Order │ │ Shipment │ │ Invoice │
│ - lineItems │ │ - packages │ │ - lineItems │
│ - discounts │ │ - weight │ │ - taxAmount │
│ - customer │ │ - destination │ │ - paymentStatus│
│ - subtotal() │ │ - carrier │ │ - revenueDate │
│ │ │ - trackingNo │ │ │
│ Customer │ │ │ │ TaxRule │
│ - preferences │ │ Address │ │ - jurisdiction │
│ - loyaltyTier │ │ - validated │ │ - rate │
└────────────────┘ └────────────────┘ └────────────────┘Notice: the same real-world concept ("Order") becomes three different models — Order, Shipment, and Invoice. Each context only models what it needs. The Sales Order has no concept of package weight; the Shipping Shipment has no concept of discounts.
Key points:
- Draw boundaries where language changes — if stakeholders use the same word differently, that's a context boundary
- Each context has its own model of shared concepts — do not try to create a single unified "Order" model
- Contexts communicate through well-defined integration points, not shared databases
---
Context Mapping
With three bounded contexts identified, define how they integrate:
┌──────────┐ OrderPlaced event ┌──────────┐
│ Sales │ ──────────────────▶ │ Shipping │
│ (upstream)│ Published Language │(downstream)│
└──────────┘ └──────────┘
│ │
│ OrderPlaced event │ ShipmentDispatched event
│ Published Language │ Published Language
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Accounting │ │ Notification │
│ (downstream) │ │ (downstream) │
└──────────────┘ └──────────────┘Integration Patterns Applied
Sales → Shipping: Published Language Sales publishes an OrderPlaced event with a well-defined schema. Shipping subscribes and translates the event into its own Shipment model. Sales does not know or care how Shipping interprets the data.
Sales → Accounting: Published Language with ACL Accounting consumes the same OrderPlaced event but applies an Anti-Corruption Layer (ACL) to translate Sales concepts into accounting terms. The ACL maps Order.lineItems and Order.discounts into Invoice.lineItems with proper tax calculations. This protects the Accounting model from changes in the Sales domain.
Shipping → Notification: Customer-Supplier Notification needs specific data from Shipping (tracking numbers, estimated delivery). Shipping agrees to include these fields in its events — a Customer-Supplier relationship where Notification's needs influence Shipping's event schema.
Key points:
- Published Language decouples contexts through shared event schemas
- Anti-Corruption Layers protect downstream contexts from upstream model changes
- Customer-Supplier relationships are appropriate when the downstream context has negotiating power
- Avoid Shared Kernel unless contexts are maintained by the same team — shared code creates coupling
---
Aggregate Design
The Sales context needs an Order aggregate. Apply Vaughn Vernon's four rules:
Rule 1: Model True Invariants
The business rule: "An order cannot be submitted if it has no line items, and the total must not exceed the customer's credit limit."
These invariants must be enforced within a single transaction. The Order aggregate protects them:
Order (Aggregate Root)
├── orderId: OrderId
├── customerId: CustomerId ← reference by ID, not embedded Customer
├── status: OrderStatus
├── lines: Set<OrderLine> ← owned by the aggregate
│ ├── productId: ProductId ← reference by ID, not embedded Product
│ ├── quantity: Quantity ← value object
│ └── unitPrice: Money ← value object
└── submit() ← enforces both invariantsRule 2: Design Small Aggregates
The Order aggregate does NOT contain Customer, Product, or ShippingAddress entities. It only references them by ID. The aggregate root plus its value objects and owned entities is the entire boundary.
Wrong: Embedding Customer inside Order so you can check the credit limit. This creates a massive aggregate and contention when multiple orders reference the same customer.
Right: The submit() method accepts a CreditLimit value object (looked up by the application service before calling the aggregate). The aggregate validates against it without owning the Customer.
Rule 3: Reference Other Aggregates by ID
customerId: CustomerId and productId: ProductId are typed IDs, not entity references. This means:
- No cascading persistence — Order and Customer have independent lifecycles
- No lock contention — modifying a Customer doesn't lock any Orders
- Clear module boundaries — Order doesn't need Customer's database table
Rule 4: Use Eventual Consistency Outside the Boundary
When an Order is submitted, the Inventory service must reserve stock. This happens asynchronously:
1. Order.submit() registers an OrderSubmitted domain event 2. The application service persists the Order (single transaction) 3. An event handler picks up OrderSubmitted and calls the Inventory context 4. If Inventory cannot reserve stock, it publishes ReservationFailed 5. A compensating handler moves the Order back to PENDING_REVIEW
Key points:
- Keep aggregates small — most should be a root entity plus value objects
- Reference other aggregates by ID, never by direct object reference
- Enforce invariants within the aggregate boundary in a single transaction
- Use domain events and eventual consistency for cross-aggregate coordination
---
Modular Monolith vs Microservices Decision
The e-commerce team (12 developers) has identified four bounded contexts. Should they deploy as microservices or a modular monolith?
Decision Matrix
| Factor | Current State | Favors |
|---|---|---|
| Team size | 12 developers, 2 teams | Modular monolith |
| Domain boundaries | Recently identified, not battle-tested | Modular monolith |
| Scaling needs | Uniform traffic, no hotspots | Modular monolith |
| Data consistency | Several cross-context transactions needed | Modular monolith |
| Deployment cadence | Weekly releases, shared schedule | Modular monolith |
| DevOps maturity | Basic CI/CD, no service mesh | Modular monolith |
| Time to market | Product launch in 3 months | Modular monolith |
Recommendation: Modular Monolith
Start with a modular monolith using Spring Modulith. Each bounded context becomes a module with:
- Its own package namespace (
com.acme.sales,com.acme.shipping) - Internal APIs hidden behind a public API surface
- Inter-module communication through application events
- Independent database schemas (logical separation within one database)
This preserves the option to extract microservices later. The bounded context boundaries are the same — only the deployment model changes. If the Pricing Engine eventually needs independent scaling (e.g., Black Friday traffic spikes), it can be extracted as a standalone service because the module boundary already enforces loose coupling.
When to Reconsider
Extract a microservice when:
- A specific module needs independent scaling (confirmed by metrics, not speculation)
- A team grows large enough to own a full service lifecycle (5+ developers per service)
- Deployment independence is needed (one module deploys daily, others weekly)
- The bounded context boundary has been stable for 6+ months
Key points:
- Default to modular monolith unless you have a specific, measurable reason for microservices
- Microservices add operational complexity (networking, observability, data consistency) that must be justified
- Well-defined module boundaries make future extraction straightforward
- Make the decision based on current constraints, not hypothetical future scale
DDD Anti-Patterns and Pitfalls
Common mistakes that undermine DDD benefits. Recognize and avoid these patterns.
Table of Contents
- Anemic Domain Model
- Symptoms
- Why It's Harmful
- The Fix: Rich Domain Model
- Greg Young's "Making Bubbles" Approach
- Over-Engineering
- Symptoms
- Decision Criteria: When DDD is Overkill
- Microsoft's Guidance
- The Fix
- Aggregate Design Mistakes
- Mistake 1: Aggregates Too Large
- Mistake 2: Wrong Aggregate Root
- Mistake 3: Cross-Aggregate Transactions
- Mistake 4: Direct Object References
- Leaky Abstractions
- Symptoms
- Why It's Harmful
- The Fix: Persistence Ignorance
- Misaligned Bounded Contexts
- Symptoms
- Causes
- The Fix
- Technical vs Business Domain Confusion
- Symptoms
- Examples
- The Fix
- Tactical Without Strategic
- Symptoms
- Why It Fails
- The Correct Order
- Signs of Correct Order
- Quick Anti-Pattern Checklist
---
Anemic Domain Model
The most common DDD anti-pattern. Domain objects become data bags with getters/setters; all logic lives in services.
Symptoms
- Domain classes are pure data containers
- All business logic in Service/Manager classes
- Domain objects can exist in invalid states
- Setters change state without business rules
- Code like
order.setStatus(Status.SHIPPED)instead oforder.ship()
Why It's Harmful
- Loses DDD's core benefit: encapsulated business logic
- Business rules scattered across services
- Hard to maintain invariants
- Code doesn't express domain concepts
The Fix: Rich Domain Model
Before (Anemic)
class Order {
Status status;
void setStatus(Status s) { this.status = s; }
}
class OrderService {
void shipOrder(Order order) {
if (order.getStatus() != Status.PAID) {
throw new IllegalStateException();
}
order.setStatus(Status.SHIPPED);
// notify, update inventory, etc.
}
}After (Rich)
class Order {
private Status status;
void ship() {
if (status != Status.PAID) {
throw new OrderNotPaidException();
}
status = Status.SHIPPED;
registerEvent(new OrderShipped(this.id));
}
}Greg Young's "Making Bubbles" Approach
Every time new requirements arrive, put the logic inside the domain model, not in service classes.
---
Over-Engineering
Applying DDD patterns where simple CRUD suffices.
Symptoms
- Separate classes for every conceivable concept
- Aggregate hierarchies for straightforward data
- Bounded contexts where natural boundaries don't exist
- Repository abstraction over 3 database tables
- Domain events for simple state changes
Decision Criteria: When DDD is Overkill
- Simple data entry with no complex rules
- No domain expert to consult
- Read-heavy application with simple writes
- Prototype or throwaway code
- CRUD-dominant functionality
Microsoft's Guidance
"DDD approaches should be applied only if implementing complex microservices with significant business rules. Simpler responsibilities, like a CRUD service, can be managed with simpler approaches."
The Fix
- Start simple, add patterns when pain emerges
- Apply tactical DDD only in core domains
- Use transaction scripts for simple use cases
- Reserve aggregates for complex invariants
---
Aggregate Design Mistakes
Mistake 1: Aggregates Too Large
Symptom: Aggregate contains many entities, causes contention and performance issues.
Cause: Modeling relationships instead of invariants.
Example: Order aggregate containing full Customer and full Product objects.
Fix: Keep aggregates small. ~70% should be root + value objects only. Reference other aggregates by ID.
Mistake 2: Wrong Aggregate Root
Symptom: Commands need to go through awkward paths to reach data.
Cause: Choosing root based on data relationships, not business invariants.
Fix: Root should be the object responsible for enforcing aggregate invariants.
Mistake 3: Cross-Aggregate Transactions
Symptom: Business logic tries to update multiple aggregates atomically.
Cause: Not accepting eventual consistency between aggregates.
Fix: One transaction = one aggregate. Use domain events for cross-aggregate coordination.
Mistake 4: Direct Object References
Symptom: Aggregate holds reference to another aggregate's entity.
class Order {
Customer customer; // Direct reference - bad
}Fix: Reference by ID only.
class Order {
CustomerId customerId; // ID reference - good
}---
Leaky Abstractions
Infrastructure concerns appearing in domain layer.
Symptoms
- Database annotations (
@Entity,@Column) on domain classes - Repository implementations exposing ORM details
- Transaction management in domain services
- Framework exceptions in domain layer
- Domain objects implement persistence interfaces
Why It's Harmful
- Domain depends on infrastructure
- Hard to test domain in isolation
- Framework changes ripple through domain
- Domain concepts polluted with technical concerns
The Fix: Persistence Ignorance
Domain Layer Contains Only
- Plain objects (entities, value objects)
- Repository interfaces (not implementations)
- Domain services with no infrastructure dependencies
- Domain events
Infrastructure Layer Contains
- Repository implementations
- ORM mappings
- Database configurations
- External service integrations
Mapping Strategy If ORM requires annotations, consider:
- Separate persistence models mapped to domain objects
- XML/external mapping configuration
- Spring Data JDBC (requires fewer annotations)
---
Misaligned Bounded Contexts
Context boundaries drawn incorrectly or not at all.
Symptoms
- Same term has different meanings across system
- Multiple teams work in same codebase, creating conflicts
- Boundaries drawn on technical lines (frontend/backend) not linguistic
- Big Ball of Mud: all concepts in one context
- Frequent merge conflicts between teams
Causes
- Technical decomposition instead of business decomposition
- Ignoring linguistic boundaries
- Premature decomposition before understanding domain
- Following database schema, not business capabilities
The Fix
Draw boundaries where language changes. When domain experts from different areas use different vocabulary for same concept, that's a boundary.
Accept duplication for autonomy. Same-named concepts in different contexts should have separate representations.
Synchronize through events, not shared objects. Contexts communicate through published events, not shared database tables or objects.
---
Technical vs Business Domain Confusion
Confusing technical concerns with business domain.
Symptoms
- "User" aggregate that's really authentication concern
- "Notification" bounded context that's infrastructure
- Technical services named as domain services
- Database schema driving domain model
- API structure dictating bounded contexts
Examples
Wrong: "Let's create a Logging bounded context"
- Logging is infrastructure, not business domain
Wrong: "Let's create a User aggregate"
- User management is often generic subdomain (auth)
- Conflating identity with business concepts (Customer, Employee)
Wrong: "Our bounded contexts are Frontend, Backend, Database"
- These are technical layers, not business capabilities
The Fix
- Ask: "Do domain experts talk about this concept?"
- Separate business domains from technical concerns
- Technical concerns go in infrastructure layer
- Business capabilities define bounded contexts
---
Tactical Without Strategic
Jumping to aggregates and repositories without strategic design.
Symptoms
- Team debates aggregate boundaries without knowing domain boundaries
- Repository interfaces before understanding context boundaries
- Domain events without context mapping
- Technical debates before business understanding
Why It Fails
- Aggregate boundaries wrong without bounded context boundaries
- Tactical patterns applied in generic subdomains (waste)
- No ubiquitous language because no strategic analysis
- Refactoring required when strategic understanding emerges
The Correct Order
1. Strategic first: Identify subdomains, classify (core/supporting/generic) 2. Context boundaries: Draw bounded contexts, create context map 3. Ubiquitous language: Establish shared vocabulary with domain experts 4. Tactical selectively: Apply patterns only in core domains
Signs of Correct Order
- Team can explain subdomain types and why
- Context map exists and is referenced
- Domain experts recognize code terminology
- Simple approaches used in generic subdomains
---
Quick Anti-Pattern Checklist
Before implementing, verify:
- [ ] Not anemic: Business logic lives in domain objects, not just services
- [ ] Not over-engineered: Complexity is justified by business rules
- [ ] Aggregates small: Most are root + value objects only
- [ ] ID references: No direct object references between aggregates
- [ ] Single transaction per aggregate: Eventual consistency outside
- [ ] Domain layer clean: No infrastructure dependencies
- [ ] Strategic foundation: Bounded contexts and subdomains identified
- [ ] Language aligned: Code uses terms domain experts recognize
Architecture Alignment with DDD
DDD aligns with several architectural styles. Choose based on team size, domain complexity, and operational maturity.
Table of Contents
- Clean Architecture
- Layers (Inner to Outer)
- The Dependency Rule
- DDD Mapping
- Hexagonal Architecture
- Core Concepts
- Port Types
- DDD Mapping
- Benefits for DDD
- Onion Architecture
- Layers (Core to Edge)
- Core Rule
- DDD Mapping
- Modular Monolith
- Structure
- Module Communication Rules
- Advantages
- When to Choose Modular Monolith
- Migration Path to Microservices
- Microservices
- DDD Alignment Principle
- One-to-One Mapping Isn't Mandatory
- Communication Patterns
- Anticorruption Layer in Microservices
- When Microservices Work Well
- When Microservices Fail
- Architecture Decision Framework
- Decision Tree
- Architecture Comparison
- Implementation Patterns
- Package Structure (Java/Spring)
- Persistence Strategies
- CQRS Integration
- Event Sourcing Integration
---
Clean Architecture
Robert Martin's layered approach with domain at center.
Layers (Inner to Outer)
1. Entities: Enterprise business rules, domain objects 2. Use Cases: Application-specific business rules (application services) 3. Interface Adapters: Controllers, presenters, gateways 4. Frameworks & Drivers: Web frameworks, databases, external services
The Dependency Rule
Source code dependencies only point inward. Inner layers know nothing about outer layers.
DDD Mapping
- Entities layer = DDD entities, value objects, aggregates
- Use Cases layer = Application services, commands, queries
- Interface Adapters = Repository implementations, controllers
- Frameworks = Spring, JPA, HTTP libraries
---
Hexagonal Architecture
Alistair Cockburn's ports and adapters approach.
Core Concepts
- Hexagon: Domain model at center
- Ports: Technology-agnostic interfaces (how domain talks to outside)
- Adapters: Translate between external systems and ports
Port Types
- Driving/Primary Ports: How outside world uses domain (API, CLI, UI)
- Driven/Secondary Ports: How domain uses infrastructure (DB, messaging)
DDD Mapping
- Hexagon core = Domain layer (entities, value objects, domain services)
- Driving ports = Application service interfaces
- Driven ports = Repository interfaces (defined in domain)
- Adapters = Controllers, JPA repositories, message handlers
Benefits for DDD
- Domain isolated from infrastructure
- Easy to swap implementations (MySQL → MongoDB)
- Natural test boundaries
- Persistence ignorance enforced
---
Onion Architecture
Jeffrey Palermo's concentric layer approach.
Layers (Core to Edge)
1. Domain Model: Entities, value objects 2. Domain Services: Business logic across aggregates 3. Application Services: Use case orchestration 4. Infrastructure: Persistence, messaging, external services
Core Rule
Outer layers depend on inner layers. Inner layers have no knowledge of outer layers.
DDD Mapping
Directly maps to DDD tactical patterns with explicit layer separation.
---
Modular Monolith
Single deployable application with internal bounded context modules.
Structure
application/
├── shared-kernel/ # Shared code (minimal)
├── order-context/ # Bounded Context
│ ├── domain/
│ │ ├── model/ # Aggregates, entities, value objects
│ │ ├── service/ # Domain services
│ │ └── repository/ # Repository interfaces
│ ├── application/
│ │ ├── service/ # Application services
│ │ ├── command/ # Commands
│ │ └── query/ # Queries
│ └── infrastructure/
│ ├── persistence/ # Repository implementations
│ └── messaging/ # Event handlers
├── inventory-context/ # Another Bounded Context
└── customer-context/ # Another Bounded ContextModule Communication Rules
1. No direct method calls between modules 2. Communication via events (preferred) or internal APIs 3. Each module owns its database schema (logical separation) 4. Shared kernel kept minimal
Advantages
- Single deployment simplicity
- No network latency for in-process calls
- Simpler transaction handling
- Lower infrastructure costs
- Easier debugging
- Prepared for future decomposition
When to Choose Modular Monolith
- Team under 20 developers
- Domain boundaries unclear
- Time-to-market critical
- Strong consistency requirements
- Limited infrastructure budget
- Early product development
Migration Path to Microservices
1. Establish strict module boundaries 2. Replace in-memory events with message broker 3. Extract high-load modules using Strangler Fig 4. Add ACLs during transition
---
Microservices
Distributed bounded contexts as independent deployable services.
DDD Alignment Principle
Each microservice should be no smaller than an aggregate and no larger than a bounded context.
One-to-One Mapping Isn't Mandatory
- Single bounded context may split for scaling (read vs write services)
- Multiple related contexts may consolidate to reduce operational overhead
- Let organizational and scaling needs drive decisions
Communication Patterns
Synchronous (REST/gRPC)
- Published contracts
- Versioned APIs
- Use for queries requiring immediate response
Asynchronous (Events)
- Preferred for decoupling
- Domain events for eventual consistency
- Saga pattern for distributed transactions
Anticorruption Layer in Microservices
Essential when integrating with:
- Legacy systems
- External services
- Systems with different domain models
- During monolith migration
When Microservices Work Well
- Bounded contexts have distinct languages
- Teams can own full contexts
- Independent scaling required
- DevOps maturity exists
- Organization structure aligns (Conway's Law)
When Microservices Fail
- Context boundaries unclear
- High coupling between contexts
- Small teams manage many services
- Consistency requirements span contexts
- Simple CRUD dominates
---
Architecture Decision Framework
Decision Tree
How complex is the domain?
├── Simple CRUD → Skip DDD, use simple layered architecture
└── Complex rules → Apply DDD
│
How large is the team?
├── < 20 developers → Modular Monolith
└── > 20 developers → Consider split
│
Are bounded context boundaries clear?
├── No → Modular Monolith (discover boundaries first)
└── Yes → Team autonomy needed?
├── No → Modular Monolith
└── Yes → Independent scaling needed?
├── No → Modular Monolith
└── Yes → MicroservicesArchitecture Comparison
| Factor | Modular Monolith | Microservices |
|---|---|---|
| Deployment | Single unit | Per service |
| Consistency | Strong (transactions) | Eventual |
| Latency | In-process | Network |
| Complexity | Application | Infrastructure |
| Team size | Small-medium | Large |
| Debugging | Simpler | Distributed tracing |
| Cost | Lower | Higher |
---
Implementation Patterns
Package Structure (Java/Spring)
Package by Layer (avoid)
com.company.app/
├── controller/
├── service/
├── repository/
└── model/Package by Feature/Bounded Context (preferred)
com.company.app/
├── order/
│ ├── domain/
│ │ ├── Order.java
│ │ ├── OrderId.java
│ │ └── OrderRepository.java (interface)
│ ├── application/
│ │ ├── PlaceOrderService.java
│ │ └── PlaceOrderCommand.java
│ └── infrastructure/
│ ├── JpaOrderRepository.java
│ └── OrderController.java
├── customer/
└── inventory/Persistence Strategies
JPA/Hibernate Challenges
- No-args constructors break value object immutability
- Setters violate encapsulation
- Lazy loading violates aggregate boundaries
@ManyToOnecreates coupling between aggregates
JPA Workarounds
- Package-private constructors
- Reference by ID (not entity) using value object wrappers
- Custom Hibernate types for strongly-typed IDs
@Converterfor complex value types
Spring Data JDBC (DDD-friendly)
- Enforces aggregate boundaries naturally
- No lazy loading
- Automatic child deletion
- Reference-by-ID is default
- Simpler mapping
Document Databases
- Natural fit: store aggregate as document
- Aggregate = document boundary
- No ORM mapping complexity
CQRS Integration
Separate Read/Write Models
- Write side: Rich domain model with aggregates
- Read side: Denormalized projections for queries
Implementation 1. Commands mutate aggregates 2. Aggregates emit domain events 3. Event handlers update read models 4. Queries read from projections
When to Apply CQRS
- Read/write patterns differ significantly
- Query optimization needed
- Event sourcing in use
- Complex reporting requirements
Apply per bounded context, not system-wide.
Event Sourcing Integration
Store Events, Not State
- Aggregate state rebuilt by replaying events
- Complete audit trail
- Time-travel debugging
- Multiple projections possible
When to Use
- Audit/compliance requirements
- Domain naturally thinks in events
- Historical state reconstruction needed
- Event-driven architecture exists
When to Avoid
- Simple CRUD
- Team unfamiliar with pattern
- Immediate consistency required for all reads
- Very long event histories
Module-level decision, not system-wide.
Strategic DDD Patterns
Strategic DDD focuses on the problem space—understanding the domain before writing code.
Table of Contents
- Subdomains
- Core Domain
- Supporting Domain
- Generic Domain
- Subdomain Identification Questions
- Bounded Contexts
- Key Principles
- Problem Space vs Solution Space
- Identifying Bounded Context Boundaries
- Bounded Context Design Checklist
- Context Mapping Patterns
- Partnership
- Shared Kernel
- Customer-Supplier
- Conformist
- Anticorruption Layer (ACL)
- Open Host Service
- Published Language
- Separate Ways
- Context Map Decision Framework
- Event Storming
- Color Coding
- Three Zoom Levels
- Facilitation Tips
- Event Storming Outcomes
---
Subdomains
Subdomains classify business capabilities by strategic importance.
Core Domain
- What: Competitive advantage—what makes the organization unique
- Investment: Maximum effort, best developers, custom from scratch
- Examples: Spotify's recommendation engine, trading platform's execution logic, ad platform's optimization
- Decision: If competitors could buy this off-the-shelf, it's not core
Supporting Domain
- What: Necessary for core to function but doesn't differentiate
- Investment: Custom development, quality tradeoffs acceptable
- Examples: E-commerce inventory management, streaming playlist management
- Decision: Required but no market advantage
Generic Domain
- What: Commodity functionality, all companies operate identically
- Investment: Buy off-the-shelf, open-source, or outsource
- Examples: Authentication, email notifications, accounting (regulated)
- Note: Same capability can be different types for different companies (identity is generic for e-commerce but core for Okta)
Subdomain Identification Questions
1. What makes us different from competitors? 2. What would we never outsource? 3. Where do domain experts spend their time? 4. What capabilities could we buy instead of build?
---
Bounded Contexts
A bounded context is where a domain model and ubiquitous language remain consistent.
Key Principles
- Same term, different meaning: "Order" means different things in Sales, Shipping, Accounting
- Each context owns its model: Duplicating concepts across contexts is acceptable
- Linguistic boundary: When language changes, you've crossed a boundary
Problem Space vs Solution Space
- Subdomain = Problem space (what problems exist)
- Bounded Context = Solution space (how we solve them)
- Not necessarily 1:1—a subdomain can have multiple bounded contexts
Identifying Bounded Context Boundaries
Language signals:
- Terms have different meanings to different teams
- Domain experts from different areas use different vocabulary
- Confusion when teams discuss the same concept
Organizational signals:
- Different teams own different parts
- Different business processes
- Different rates of change
Technical signals:
- Different data models for same concept
- Multiple motivations for change in one area
- Teams stepping on each other's code
Bounded Context Design Checklist
- [ ] Single ubiquitous language within context
- [ ] Clear owner (team or individual)
- [ ] Explicit public interface for external communication
- [ ] Internal model hidden from other contexts
- [ ] Context map documents relationships
---
Context Mapping Patterns
Context maps document relationships between bounded contexts, from tight to loose coupling.
Partnership
- When: Two teams must succeed or fail together
- How: Coordinated planning, joint meetings
- Tradeoff: High coordination cost, tight coupling
- Use when: Contexts evolve together frequently
Shared Kernel
- When: Small, explicit subset of model shared between teams
- How: Shared code/schema, changes require consultation
- Tradeoff: Coupling through shared code, coordination overhead
- Keep it minimal: Large shared kernels become Big Ball of Mud
Customer-Supplier
- When: Upstream team accommodates downstream needs
- How: Downstream specifies requirements, upstream prioritizes
- Tradeoff: Upstream team must balance multiple customers
- Formalize: Explicit contracts, regular communication
Conformist
- When: Downstream adopts upstream model wholesale
- How: No translation layer, direct use of upstream model
- Tradeoff: Tight coupling, no protection from upstream changes
- Use when: Upstream model is good enough, translation cost too high
Anticorruption Layer (ACL)
- When: Upstream model would corrupt downstream model integrity
- How: Translation layer isolates downstream from foreign concepts
- Tradeoff: Development overhead, maintenance of translation
- Essential for: Legacy integration, external services, model mismatch
Open Host Service
- When: Many consumers need the same upstream functionality
- How: Well-defined API protocol, versioned contracts
- Tradeoff: API stability requirements, versioning complexity
- Use when: Multiple downstream contexts need same data
Published Language
- When: Standard format for exchange between contexts
- How: Documented schema (JSON, Protobuf, industry standard)
- Combine with: Open Host Service for complete solution
Separate Ways
- When: No integration at all
- How: Contexts evolve independently
- Use when: Integration cost exceeds benefit, truly separate domains
Context Map Decision Framework
Do models share concepts?
├── No → Separate Ways
└── Yes → Do we control both sides?
├── No → Must we adapt?
│ ├── Yes, model is good → Conformist
│ └── No, protect our model → ACL
└── Yes → How tightly coupled?
├── Very tight → Partnership or Shared Kernel
└── Upstream/downstream → Customer-Supplier + Open Host---
Event Storming
Alberto Brandolini's technique for rapid domain discovery.
Color Coding
- Orange: Domain events (past tense: "Order Placed")
- Blue: Commands (what triggers events)
- Yellow: Actors (who/what issues commands)
- Pink: Hot spots (problems, questions, risks)
- Purple: Policies (reactions: "When X happens, do Y")
- Green: Read models (data needed for decisions)
- Pale yellow: Aggregates (clusters of events)
Three Zoom Levels
Big Picture (1-2 days)
- 25-30 participants across business
- Explore entire business lines
- Identify opportunities and boundaries
- Output: Subdomain candidates, bounded context hints
Process Modeling (half day)
- Focused on specific business process
- More rigorous grammar
- Output: Detailed process flow, policies identified
Software Design (hours)
- Add aggregates and commands
- Bridge business and technical concerns
- Output: Aggregate candidates, bounded context boundaries
Facilitation Tips
1. Start with domain events (what happened?) 2. Use reverse narrative—work backward to find hidden events 3. Mark hot spots, don't solve them immediately 4. Look for pivotal events that change business state 5. Identify where language changes (bounded context hints)
Event Storming Outcomes
- Shared understanding across business and tech
- Discovered subdomain boundaries
- Aggregate identification
- Foundation for ubiquitous language
- Hot spots for further investigation
Tactical DDD Patterns
Tactical patterns implement domain logic within bounded context boundaries. Apply selectively in core domains only.
Table of Contents
- Entities
- Characteristics
- Examples
- Entity Design Guidelines
- When to Use Entity
- Value Objects
- Characteristics
- Examples
- Value Object Design Guidelines
- Common Mistake: Primitive Obsession
- When to Use Value Object
- Aggregates
- Core Concepts
- Vaughn Vernon's Design Rules
- Aggregate Design Process
- Common Aggregate Mistakes
- Aggregate Size Heuristic
- Domain Services
- Characteristics
- When to Use Domain Service
- Examples
- Domain Service Guidelines
- Application Services
- Characteristics
- Responsibilities
- Example Structure
- Domain Service vs Application Service
- Repositories
- Characteristics
- Interface Design
- Repository vs DAO
- Repository Guidelines
- Factories
- When to Use Factory
- When Constructor Suffices
- Factory Patterns
- Domain Events
- Characteristics
- Domain Event Structure
- Domain Events vs Integration Events
- Event Publishing Pattern
- Domain Event Guidelines
---
Entities
Objects defined by unique identity maintained throughout their lifecycle.
Characteristics
- Identity-based equality (same ID = same entity)
- Mutable state
- Trackable history
- Lifecycle (created, modified, archived)
Examples
Customer(ID=123 remains same customer as name changes)Order(tracked from creation to fulfillment)Account(balance changes, identity persists)Employee(role changes, same person)
Entity Design Guidelines
1. Identity should be immutable after creation 2. Use strongly-typed IDs (CustomerId not long) 3. Encapsulate state changes in behavior methods 4. Validate invariants on state changes
When to Use Entity
- Need to track object through time
- Domain experts reference it as uniquely identifiable
- Object has a lifecycle with state transitions
- Equality based on identity, not attributes
---
Value Objects
Objects defined entirely by their attributes—no conceptual identity.
Characteristics
- Structural equality (same attributes = equal objects)
- Immutable (changes create new instances)
- No identity
- Freely interchangeable
Examples
Money($100 USD is identical to any other $100 USD)Address(123 Main St is the same address everywhere)DateRange(Jan 1-15 is equal to another Jan 1-15)Email(encapsulates format validation)Coordinates(lat/long pair)
Value Object Design Guidelines
1. Make immutable—no setters 2. Implement structural equality 3. Use for attributes that describe entities 4. Encapsulate validation in constructor 5. Provide behavior methods that return new instances
Common Mistake: Primitive Obsession
// Bad: Primitives leak validation everywhere
String email = customer.getEmail();
// Good: Value object encapsulates rules
Email email = customer.getEmail();
// Email class validates format, provides behaviorWhen to Use Value Object
- Only attribute values matter
- Objects with same values are interchangeable
- Concept represents descriptive aspect
- No need to track through time
- Default choice—use entities only when identity required
---
Aggregates
Cluster of domain objects treated as single unit for data changes.
Core Concepts
- Aggregate Root: Single entry point, only externally referenceable object
- Boundary: Defines transactional consistency scope
- Invariants: Business rules enforced within boundary
Vaughn Vernon's Design Rules
Rule 1: Model true invariants in consistency boundaries
- Only include objects that must be immediately consistent
- If consistency can be eventual, separate aggregates
Rule 2: Design small aggregates
- Large aggregates never perform or scale well
- ~70% should be just root entity + value objects
- ~30% should have 2-3 total entities maximum
Rule 3: Reference other aggregates by ID only
// Bad: Direct object reference
class Order {
Customer customer; // Creates coupling
}
// Good: ID reference
class Order {
CustomerId customerId; // Decoupled
}Rule 4: Use eventual consistency outside boundary
- One transaction = one aggregate modification
- Cross-aggregate consistency via domain events
Aggregate Design Process
1. Identify cluster of related objects 2. Determine which invariants must be immediately consistent 3. Choose aggregate root (commands go through root) 4. Draw minimal boundary around true invariants 5. Everything else becomes separate aggregate with ID reference
Common Aggregate Mistakes
- Too large: Modeling relationships instead of rules
- Wrong root: Choosing based on data, not invariants
- Cross-aggregate transactions: Trying to update multiple in one transaction
- Direct references: Object links instead of ID references
Aggregate Size Heuristic
If aggregate contains more than 3 entities, question whether all invariants truly require immediate consistency.
---
Domain Services
Stateless operations implementing domain logic that doesn't belong in entities or value objects.
Characteristics
- Contain business logic
- Operate on domain objects
- Use ubiquitous language
- Live in domain layer
- No state
When to Use Domain Service
- Operation involves multiple aggregates
- Logic doesn't naturally fit in single entity
- Business concept is a process, not a thing
Examples
// Transfer between accounts requires both accounts
TransferService.transfer(fromAccount, toAccount, amount)
// Pricing involves product, customer tier, promotions
PricingService.calculatePrice(product, customer, promotions)
// Shipping calculation requires address, items, carrier
ShippingService.calculateCost(address, items, carrier)Domain Service Guidelines
1. Name using ubiquitous language 2. Keep stateless 3. Operate on domain objects, not DTOs 4. Don't put CRUD operations here
---
Application Services
Orchestrate use cases without containing domain logic.
Characteristics
- Orchestration only, no business rules
- Work with DTOs and commands
- Manage transaction boundaries
- Entry point from presentation layer
- Fetch entities, delegate to domain, persist changes
Responsibilities
1. Receive command/request 2. Fetch required aggregates from repositories 3. Execute domain operations 4. Persist changes 5. Publish integration events 6. Return result
Example Structure
class PlaceOrderService {
execute(PlaceOrderCommand command) {
// 1. Fetch aggregates
customer = customerRepository.findById(command.customerId);
product = productRepository.findById(command.productId);
// 2. Execute domain logic (in domain, not here)
order = customer.placeOrder(product, command.quantity);
// 3. Persist
orderRepository.save(order);
// 4. Publish events
eventPublisher.publish(order.getDomainEvents());
// 5. Return result
return OrderDto.from(order);
}
}Domain Service vs Application Service
| Aspect | Domain Service | Application Service |
|---|---|---|
| Contains | Business logic | Orchestration |
| Language | Ubiquitous | Use cases |
| Layer | Domain | Application |
| State | Stateless | Stateless |
| Works with | Domain objects | DTOs, Commands |
Rule: If business rules appear in application service, move them to domain service or entity.
---
Repositories
Collection-oriented interfaces for accessing aggregates.
Characteristics
- Abstract persistence details
- Interface in domain layer
- Implementation in infrastructure
- Work with aggregates, not tables
- Use ubiquitous language
Interface Design
interface OrderRepository {
Order findById(OrderId id);
List<Order> findPendingOrders(); // Business language
List<Order> findByCustomer(CustomerId id);
void save(Order order);
void delete(Order order);
}Repository vs DAO
| Aspect | Repository | DAO |
|---|---|---|
| Scope | Aggregate | Table/Entity |
| Language | Business terms | Technical terms |
| Returns | Domain objects | Data objects |
| Interface location | Domain layer | Infrastructure |
Repository Guidelines
1. One repository per aggregate root 2. Interface uses ubiquitous language 3. Return domain objects, not entities 4. Abstract query details from domain 5. Handle aggregate reconstitution
---
Factories
Encapsulate complex aggregate creation.
When to Use Factory
- Construction requires multiple steps
- Business rules apply during creation
- Structure varies based on input
- Creation logic is complex
When Constructor Suffices
- Simple aggregates
- No special creation logic
- Few required parameters
Factory Patterns
Factory Method (on aggregate or separate class)
class Order {
static Order createWithDiscount(Customer customer, DiscountCode code) {
// Apply discount rules during creation
order = new Order(customer);
order.applyDiscount(code);
return order;
}
}Factory Service (when creation needs external data)
class OrderFactory {
Order create(CustomerId customerId, List<ProductId> productIds) {
customer = customerRepo.findById(customerId);
products = productRepo.findByIds(productIds);
// Complex assembly with validation
return new Order(customer, products);
}
}---
Domain Events
Significant occurrences that happened in the past.
Characteristics
- Named in past tense:
OrderPlaced,PaymentProcessed - Immutable record of what happened
- Contain relevant data at time of occurrence
- Enable loose coupling between aggregates
Domain Event Structure
class OrderPlaced {
OrderId orderId;
CustomerId customerId;
Money totalAmount;
Instant occurredAt;
// All data needed by handlers
}Domain Events vs Integration Events
| Aspect | Domain Event | Integration Event |
|---|---|---|
| Scope | Within bounded context | Across contexts |
| Transport | In-memory | Message broker |
| Timing | Often synchronous | Asynchronous |
| Coupling | Loose within context | Cross-context |
Event Publishing Pattern
1. Aggregate records event during operation 2. Application service retrieves events after save 3. Events dispatched to handlers 4. Handlers update other aggregates or projections
Domain Event Guidelines
1. Name using ubiquitous language 2. Include all data handlers need 3. Make immutable 4. Record timestamp 5. Consider versioning for evolution
Domain-Driven Design Troubleshooting
Common issues and solutions for DDD implementation.
Common Issues
Issue: Anemic Domain Model
Symptom: Entities are just data holders with getters/setters, all logic in services
Cause: Treating domain objects as data structures rather than behavior-rich objects
Solution:
// Before - Anemic domain model (ANTI-PATTERN)
public class Order {
private OrderStatus status;
private List<OrderItem> items;
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
}
public class OrderService {
public void cancelOrder(Order order) {
if (order.getStatus() == SHIPPED) {
throw new IllegalStateException("Cannot cancel shipped order");
}
order.setStatus(CANCELLED);
// refund logic...
}
}
// After - Rich domain model
public class Order {
private OrderStatus status;
private List<OrderItem> items;
public void cancel() {
if (this.status == SHIPPED) {
throw new OrderCannotBeCancelledException("Order already shipped");
}
this.status = CANCELLED;
// Domain event could be raised here
}
}---
Issue: God Aggregate (Too Large)
Symptom: One aggregate contains too many entities, transactions are slow, contention issues
Cause: Modeling entire domain in a single aggregate instead of identifying true invariants
Solution:
// Before - God aggregate (ANTI-PATTERN)
public class Customer {
private List<Order> orders; // Could be millions
private List<Address> addresses;
private List<PaymentMethod> payments;
private ShoppingCart cart;
private WishList wishList;
private LoyaltyAccount loyalty;
}
// After - Small, focused aggregates with ID references
public class Customer {
private CustomerId id;
private CustomerProfile profile;
private List<AddressId> addressIds; // Reference by ID
private DefaultPaymentMethodId defaultPayment;
}
public class Order {
private OrderId id;
private CustomerId customerId; // Reference by ID
private List<OrderLine> lines; // True invariant: order totals
}
public class ShoppingCart {
private CustomerId customerId; // Separate aggregate
private List<CartItem> items;
}Rule of thumb: ~70% of aggregates should be just root + value objects.
---
Issue: Cross-Aggregate Transactions
Symptom: Business operation requires updating multiple aggregates atomically
Cause: Wrong aggregate boundaries or misunderstanding eventual consistency
Solution:
// Before - Cross-aggregate transaction (ANTI-PATTERN)
@Transactional
public void placeOrder(Order order, Inventory inventory, Customer customer) {
orderRepository.save(order);
inventory.decrementStock(order.getItems()); // Different aggregate!
customer.addLoyaltyPoints(order.getTotal()); // Different aggregate!
}
// After - Domain events with eventual consistency
public class Order extends AbstractAggregateRoot<Order> {
public void place() {
// ... order logic
registerEvent(new OrderPlacedEvent(this.id, this.items, this.total));
}
}
@Component
public class InventoryEventHandler {
@EventListener
public void on(OrderPlacedEvent event) {
// Handle in separate transaction
inventory.decrementStock(event.items());
}
}
@Component
public class LoyaltyEventHandler {
@EventListener
public void on(OrderPlacedEvent event) {
customer.addLoyaltyPoints(event.total());
}
}---
Issue: Entity Used Where Value Object Needed
Symptom: Objects with generated IDs that don't need identity tracking
Cause: Defaulting to entities, not asking "does identity matter?"
Solution:
// Before - Entity when value object is appropriate (ANTI-PATTERN)
@Entity
public class Money {
@Id @GeneratedValue
private Long id; // Why does money need an ID?
private BigDecimal amount;
private String currency;
}
// After - Value object (immutable, no identity)
@Embeddable
public record Money(BigDecimal amount, Currency currency) {
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException();
}
return new Money(this.amount.add(other.amount), this.currency);
}
}Ask: "If two objects have the same attributes, are they the same thing?"
- Yes → Value Object
- No → Entity
---
Issue: Bounded Contexts Not Defined
Symptom: Single model tries to represent everything, terms have multiple meanings
Cause: Skipping strategic design, jumping to tactical patterns
Solution:
// Before - One model for everything (ANTI-PATTERN)
Customer
├── name, email (Identity context)
├── shippingAddresses (Shipping context)
├── creditScore, paymentTerms (Billing context)
├── purchaseHistory (Sales context)
└── supportTickets (Support context)
// After - Different models per bounded context
Identity Context: Customer { id, email, profile }
Shipping Context: Recipient { customerId, addresses }
Billing Context: Account { customerId, creditLimit, paymentTerms }
Sales Context: Buyer { customerId, preferences, history }
Support Context: Contact { customerId, tickets, satisfaction }Each context has its own Customer representation with only relevant attributes.
---
Issue: Repository Does Too Much
Symptom: Repository has business logic, complex queries, or returns DTOs
Cause: Misunderstanding repository's role as collection abstraction
Solution:
// Before - Repository with business logic (ANTI-PATTERN)
public interface OrderRepository {
List<OrderDTO> findPendingOrdersForDashboard(); // Returns DTO
void cancelExpiredOrders(); // Business logic!
BigDecimal calculateRevenueByMonth(Month month); // Reporting query
}
// After - Repository as pure collection abstraction
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
List<Order> findByStatus(OrderStatus status);
}
// Business logic in domain service or aggregate
public class OrderExpirationService {
public void cancelExpiredOrders() {
List<Order> pending = orderRepository.findByStatus(PENDING);
pending.stream()
.filter(Order::isExpired)
.forEach(order -> {
order.cancel();
orderRepository.save(order);
});
}
}
// Reporting in separate read model/CQRS query
public interface OrderReportingQuery {
RevenueReport getRevenueByMonth(Month month);
}---
Issue: Domain Layer Has Infrastructure Dependencies
Symptom: Domain entities import JPA, Spring, or other framework annotations
Cause: Not maintaining persistence ignorance
Solution:
// Before - Domain polluted with infrastructure (ANTI-PATTERN)
@Entity // JPA annotation in domain
@Table(name = "orders")
public class Order {
@Id @GeneratedValue
private Long id;
@Autowired // Spring in domain!
private EmailService emailService;
public void confirm() {
emailService.sendConfirmation(this); // Infrastructure in domain
}
}
// After - Clean domain with infrastructure in adapters
// Domain layer (no framework imports)
public class Order {
private OrderId id;
public OrderConfirmedEvent confirm() {
// Pure domain logic
return new OrderConfirmedEvent(this.id, this.customerEmail);
}
}
// Infrastructure layer (JPA adapter)
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id private String id;
// ... JPA mappings
}
// Application layer handles events
@EventListener
public void onOrderConfirmed(OrderConfirmedEvent event) {
emailService.sendConfirmation(event);
}---
Strategic Design Issues
Missing Ubiquitous Language
Symptom: Code uses technical terms, business stakeholders don't understand it
Solution: Rename code to match business terminology:
// Before - Technical naming
CustomerDataTransferObject, processTransaction(), handleEvent()
// After - Ubiquitous language
CustomerProfile, placeOrder(), orderWasShipped()Wrong Context Boundaries
Symptom: Teams stepping on each other, constant coordination needed
Solution: Boundaries should align with:
- Team ownership
- Language changes (same term, different meaning)
- Business capability boundaries
- Rate of change
Domain-Driven Design Workflow
Detailed step-by-step process for applying DDD to a software system.
---
Step 1: Domain Discovery
Identify subdomains and their strategic importance.
1a. Engage Domain Experts
- Schedule domain expert interviews or Event Storming sessions
- Focus on understanding business processes, not technical implementation
- Document domain terminology — this becomes your ubiquitous language
1b. Identify Subdomains
Classify each area of the business:
| Subdomain Type | Investment Level | Strategy |
|---|---|---|
| Core | Maximum — competitive advantage | Custom development with DDD tactical patterns |
| Supporting | Moderate — needed but not differentiating | Simpler patterns, quality tradeoffs OK |
| Generic | Minimal — commodity | Buy/outsource (auth, email, payments) |
1c. Event Storming (Optional but Recommended)
1. Gather domain experts and developers 2. Identify domain events (past tense: "Order Placed", "Payment Received") 3. Group events by business process 4. Identify commands that trigger events 5. Identify aggregates that handle commands 6. Draw boundaries where language or process changes
Output: Subdomain map with strategic classification and initial event flow.
---
Step 2: Bounded Context Definition
Draw boundaries where the ubiquitous language changes.
2a. Identify Language Boundaries
Signs you need a boundary:
- Same word means different things to different teams (e.g., "Account" in billing vs. authentication)
- Different teams own different parts of the process
- Data models diverge significantly
- Deployment or scaling requirements differ
2b. Define Context Boundaries
For each bounded context:
- Name it using the ubiquitous language of that context
- List the aggregates, entities, and value objects it owns
- Define its public API (what it exposes to other contexts)
- Identify its internal model (hidden from other contexts)
2c. Create Context Map Diagram
Visualize relationships between contexts:
+---------------+ +---------------+
| Orders |---->| Inventory |
| (Core) | | (Supporting) |
+---------------+ +---------------+
|
v
+---------------+
| Payments |
| (Generic) |
+---------------+Output: Context map showing boundaries and relationships.
---
Step 3: Context Mapping
Define integration patterns between bounded contexts.
3a. Choose Integration Pattern
| Pattern | When to Use | Direction |
|---|---|---|
| Shared Kernel | Two teams co-own a small model | Bidirectional |
| Customer-Supplier | Upstream provides, downstream consumes | Upstream to Downstream |
| Conformist | Downstream adopts upstream's model as-is | Upstream to Downstream |
| Anti-Corruption Layer (ACL) | Protect your model from external changes | Downstream defense |
| Published Language | Standardized format (e.g., JSON schema) | Between contexts |
| Open Host Service | Expose a well-defined protocol | Upstream provides |
| Separate Ways | No integration needed | Independent |
3b. Document Integration Contracts
For each integration: 1. Which contexts are involved? 2. What data flows between them? 3. What pattern is used? 4. Who owns the contract? 5. How are changes negotiated?
3c. Implement Integration
- Events (preferred): Domain events for loose coupling — see
spring-boot-modulithskill - API calls: REST/gRPC for synchronous needs — see
spring-boot-web-apiskill - ACL: Translation layer at context boundary
Output: Integration pattern decisions for each context relationship.
---
Step 4: Architecture Selection
Choose the right architecture for your bounded contexts.
4a. Evaluate Constraints
| Factor | Modular Monolith | Microservices |
|---|---|---|
| Team size | < 20 developers | 20+ developers |
| Domain clarity | Boundaries still evolving | Well-understood boundaries |
| Time-to-market | Critical — ship fast | Can invest in infrastructure |
| Consistency | Strong consistency needed | Eventual consistency acceptable |
| DevOps maturity | Low — shared deployment | High — CI/CD per service |
| Scaling needs | Uniform scaling OK | Independent scaling required |
4b. Make the Decision
Start with modular monolith (recommended for most projects):
- Use Spring Modulith for enforced boundaries — see
spring-boot-modulithskill - Evolve to microservices later if needed (contexts are already separated)
- Lower operational complexity, faster development
Choose microservices when:
- Teams can independently own, deploy, and scale their context
- Bounded contexts have clearly distinct data stores
- Independent scaling is a hard requirement
4c. Apply Architecture Pattern
Within each bounded context, choose internal architecture:
| Pattern | Best For |
|---|---|
| Hexagonal (Ports & Adapters) | Core domains with complex business logic |
| Clean Architecture | Similar to hexagonal, explicit use-case layer |
| Transaction Script | Supporting/generic domains with simple CRUD |
Output: Architecture decision for each bounded context.
---
Step 5: Tactical Implementation
Apply DDD patterns within core domains.
5a. Design Aggregates
Apply Vaughn Vernon's 4 rules: 1. Model true invariants in consistency boundaries 2. Design small aggregates (~70% should be root + value objects only) 3. Reference other aggregates by ID only 4. Use eventual consistency outside the boundary
5b. Implement Core Building Blocks
| Building Block | When to Use | Spring Boot Implementation |
|---|---|---|
| Entity | Identity matters, tracked over time | JPA @Entity — see spring-boot-data-ddd |
| Value Object | Defined by attributes, immutable | @Embedded or records — see spring-boot-data-ddd |
| Aggregate Root | Consistency boundary entry point | AbstractAggregateRoot<T> — see spring-boot-data-ddd |
| Domain Service | Logic spanning multiple aggregates | @Service in domain package |
| Domain Event | Cross-aggregate/cross-context communication | Java record + @ApplicationModuleListener — see spring-boot-modulith |
| Repository | Aggregate persistence | ListCrudRepository — see spring-boot-data-ddd |
5c. Validate Implementation
- [ ] Each aggregate enforces its own invariants
- [ ] Value objects are immutable (no setters)
- [ ] Aggregates reference each other by ID only
- [ ] One aggregate per transaction
- [ ] Domain layer has no infrastructure dependencies
- [ ] Repository exists only for aggregate roots
- [ ] Domain events handle cross-aggregate side effects
Output: Working implementation of DDD patterns in core domains.
---
Common Pitfalls
| Pitfall | Prevention |
|---|---|
| Starting with tactical patterns | Do strategic design (Steps 1-4) first |
| Applying DDD everywhere | Only use in core domains; CRUD is fine elsewhere |
| Anemic domain model | Put behavior IN entities, not just in services |
| Big aggregates | Keep small; reference by ID |
| Ignoring ubiquitous language | Code should read like business language |
| Premature microservices | Start monolith, extract later |
See references/ANTI-PATTERNS.md for detailed anti-pattern analysis.