How to Build Software That Bends Without Breaking
“Good architecture allows you to change your mind without changing the world.”
Prologue: The Cathedral of Complexity
It started with a simple user registration feature.
Six months later, the codebase had grown into something unrecognizable. Thirty-seven classes to create a user account. Six different DTOs for what was essentially name, email, and password. A domain event system that published “UserRegistered” events nobody consumed. Bounded contexts that mapped 1:1 to database tables.
The team had followed every DDD pattern in the book. They’d created aggregates for entities with no invariants. They’d defined repositories with specification patterns for simple CRUD. They’d built an event sourcing system for data that never changed.
The result? A codebase so complex that adding a new field required touching 12 files. Unit tests that took 3 minutes to start because they needed the Spring context. New developers who spent their first week just trying to understand where business logic lived.
This is the dark side of Domain-Driven Design. When followed dogmatically, it doesn’t simplify - it bloats.
Imagine you’re constructing a house. The Domain is the foundation - poured concrete, load-bearing walls, the very skeleton that keeps everything standing. The Application layer is the plumbing and electrical—connecting systems, directing flow, but never deciding where rooms should go. Infrastructure is the facade and fixtures—the paint, the windows, the doorknobs that can be swapped without touching the structure.
For decades, developers have been handed rigid architectural blueprints. DDD purists insist on every wall, every beam, every measurement following sacred patterns. But pragmatic DDD asks a different question: Does this room need a cathedral ceiling, or will standard height do?
The revolution is not in the complexity - it is in the selective application. In this series, we will explore how to build systems that are robust without being over-engineered, clear without being bureaucratic, and maintainable without requiring a PhD in design patterns.
When DDD Saves You (And When It Doesn’t)
The Decision Matrix
Before writing a single line of code, determine if DDD fits your context:
Use DDD when
Complex business rules, many paths through workflows
Centralized business logic prevents scattering.
For exampleE-commerce checkout with discounts, taxes and inventory
Long-term project (2+ years), evolving requirements
Boundaries protect against future changes.
For exampleEnterprise billing system with changing regulations
Multiple teams, changing developers
Clear structure reduces onboarding friction.
For exampleBanking platform with 5+ teams contributing
Keep it simple when
Simple CRUD, no complex rules
Transaction scripts are faster to write and maintain.
For exampleBlog post CMS: create, read, update and delete only
Rapid prototype or MVP
YAGNI (You Are not Gonna Need It): validate first.
For exampleStartup landing page with a contact form
Team unfamiliar with DDD
Wrong abstractions are worse than no abstractions.
For exampleJunior team with a tight deadline
The Evolution Strategy
Don’t start with DDD. Start simple, evolve as complexity demands:
- Weeks 1–4Transaction script
- Months 2–3Anemic domain model
- Months 4–6Rich domain model
- Year 1+Full DDDonly if needed
Transaction Script Pattern: Business logic lives in service methods, not domain objects. Each method represents a single transaction (e.g., registerUser(), processOrder()). Simple, fast to write, perfect for CRUD apps. Part 3 shows a before/after example.
Start at your current complexity level, not where you think you’ll be.
Case Study: Bounded Contexts Decision
Consider a small team building a user management system:
Scenario: 3 developers, 1 domain language, straightforward authentication
Over-engineered approach:
@Context("UserManagement")
public class UserContext { ... }
@Context("Authentication")
public class AuthContext { ... }Pragmatic approach:
/domain/user/User.java
/domain/auth/Password.javaDecision factors:
- Team size: Small (3 developers)
- Domain complexity: Low (basic CRUD)
- Language differences: None (unified terms)
- Result: Package-level separation is sufficient
Beyond the Dogma - When Simplicity Wins
The Anti-Pattern Gallery
While DDD patterns are powerful, misapplication causes damage:
Real-World Simplifications
We skipped CQRS because:
- Our read/write ratio was 70/30 (not extreme)
- Reports were simple SQL queries
- Adding complexity wasn’t justified
We skipped Event Sourcing because:
- Our audit requirements were met by database auditing
- Replay scenarios were rare debugging tools
- The complexity cost exceeded the benefit
We used enums instead of entities for:
- Status codes (ACTIVE, INACTIVE)
- Configuration flags
- Simple reference data
public enum OrderStatus {
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}@Entity
public class OrderStatusEntity {
@Id private Long id;
private String code;
private String name;
}When to Simplify Back
You covered “when not to use DDD” but knowing when to remove DDD is equally important:
Consider simplifying when:
- Aggregates have only 1-2 methods with no complex rules
- Use cases do nothing but call
repository.save() - You spend more time maintaining layers than adding features
- Team velocity drops after DDD adoption
- Simple operations require touching 5+ files
Signs you’ve over-engineered:
public class StatusAggregate {
private StatusId id;
private StatusName name;
public void activate() { this.status = Status.ACTIVE; }
public void deactivate() { this.status = Status.INACTIVE; }
// No invariants, no rules - just state changes
}public enum Status { ACTIVE, INACTIVE }Migration path back:
- Consolidate thin use cases into services
- Remove unnecessary value object wrappers
- Simplify aggregate boundaries
- Document why you simplified (for future you)
Remember: Architecture serves the code, not the other way around.