What is Domain-Driven Design?
Domain-Driven Design (DDD) is a software design approach that focuses on modeling software to match the business domain. The Blackjack API uses DDD tactical patterns to create a rich, expressive domain model.DDD Building Blocks
Aggregates
Aggregates are clusters of domain objects that can be treated as a single unit. Each aggregate has a root entity that controls access to the entire aggregate.Game Aggregate
Aggregate Root:Game
Location: domain/model/Game.java:1-120
Why is Game an Aggregate Root?
Why is Game an Aggregate Root?
Game is an aggregate root because:
- Consistency Boundary: All blackjack rules (hit, stand, bust) must be enforced through the
Gameobject - Transaction Boundary: A game and its hands are saved/loaded as a single unit
- External Access: Other parts of the system reference games by
GameId, never directly accessingHandorDeck - Invariant Protection: The game ensures all business rules are maintained (e.g., can’t hit after standing)
Game Business Logic
The aggregate root encapsulates all blackjack game logic: Location:domain/model/Game.java:55-68
Immutability: Notice that
hit() and stand() return new Game instances rather than modifying the existing one. This prevents bugs and makes the code easier to reason about.Player Aggregate
Aggregate Root:Player
Location: domain/model/Player.java:1-40
Player is a separate aggregate because:
- It has a different lifecycle than games (players persist across multiple games)
- It has its own invariants (name validation, non-negative stats)
- It can be referenced from games via
PlayerId
Entities
Entities are objects with a distinct identity that persists over time, even if attributes change.Characteristics of Entities
| Property | Description | Example |
|---|---|---|
| Identity | Unique identifier | GameId, PlayerId |
| Mutable (conceptually) | Attributes can change | Player’s name or stats can change |
| Equality by ID | Two entities are equal if IDs match | Two Player objects with same PlayerId |
Hand Entity
Location:domain/model/Hand.java:1-46
Hand implements blackjack-specific logic like:- Ace value adjustment (11 or 1)
- Blackjack detection (21 with exactly 2 cards)
- Bust detection (score > 21)
Value Objects
Value Objects are immutable objects defined by their attributes rather than identity. Two value objects with the same attributes are considered equal.Card Value Object
Location:domain/model/Card.java:1-10
GameId Value Object
Location:domain/model/GameId.java:1-12
PlayerName Value Object
Location:domain/model/PlayerName.java:1-11
Value objects encapsulate validation logic, ensuring invalid states are impossible. You cannot create a
PlayerName that’s blank or too long.Benefits of Value Objects
Type Safety
Can’t accidentally pass a
GameId where a PlayerId is expected.Self-Validating
Validation logic lives with the value object, not scattered throughout the codebase.
Immutability
Value objects cannot be modified after creation, preventing bugs.
Domain Expressiveness
PlayerName is more meaningful than String.Domain Services
Domain services contain domain logic that doesn’t naturally belong to any entity or value object.The Blackjack API currently doesn’t have explicit domain services because all domain logic fits naturally within aggregates (
Game and Player).If we needed to implement a match-making service or tournament logic that coordinates multiple games and players, those would be domain services.When to Create a Domain Service
Create a domain service when:- Operation involves multiple aggregates
- Logic doesn’t naturally belong to a single entity
- Operation is a significant domain concept (e.g., “CalculatePlayerRanking”)
Domain Events (Implicit)
Although not explicitly implemented, the domain model has implicit events:Ubiquitous Language
DDD emphasizes using domain terminology consistently across code and conversations.Blackjack Ubiquitous Language
| Domain Term | Code Representation | Meaning |
|---|---|---|
| Game | Game aggregate | A single blackjack game session |
| Player | Player aggregate | A registered player |
| Hand | Hand entity | Cards held by player or dealer |
| Hit | Game.hit() | Draw another card |
| Stand | Game.stand() | End turn, dealer plays |
| Bust | Hand.isBust() | Score exceeds 21 |
| Blackjack | Hand.isBlackjack() | Natural 21 with 2 cards |
| Deck | Deck entity | 52-card shoe |
Notice how the code uses the same terms as domain experts would use when discussing blackjack.
Invariant Protection
Aggregates enforce business rules (invariants) to maintain consistency.Example: Game State Transitions
Location:domain/model/Game.java:110-112
- Hitting after the game has ended
- Standing in an already-finished game
- Invalid state transitions
Example: Player Stats Validation
Location:domain/model/Player.java:14
Anemic vs. Rich Domain Model
Anemic Domain Model (Anti-pattern)
Rich Domain Model (DDD Approach)
In the Blackjack API, domain logic lives in domain objects, not in services. This makes the code more maintainable and the domain model more expressive.
Persistence Ignorance
Domain models are pure Java with no persistence framework annotations. Domain Model (domain/model/Game.java):
infrastructure/persistence/mongo/MongoGameDocument.java):
Factory Methods
Domain objects use factory methods to control object creation.- Meaningful names:
Game.newGame()vsnew Game(...) - Encapsulation: Constructor is private, forcing use of factory
- Flexibility: Factory can apply business rules or defaults
DDD in Use Cases
Use cases orchestrate domain objects: Location:application/usecase/PlayMoveUseCase.java:24-38
Use cases:
- Load domain objects from repositories
- Delegate business logic to domain objects
- Save updated domain objects
- Map to result DTOs
Summary: DDD Patterns in Blackjack API
| Pattern | Implementation | Location |
|---|---|---|
| Aggregate Root | Game, Player | domain/model/ |
| Entity | Hand, Deck | domain/model/ |
| Value Object | Card, GameId, PlayerName | domain/model/ |
| Factory Method | Game.newGame(), Player.newPlayer() | domain/model/ |
| Invariant Protection | ensureInProgress(), constructor validation | Throughout domain |
| Ubiquitous Language | Domain terms in code match business terms | Everywhere |
| Rich Domain Model | Business logic in domain objects | domain/model/ |
| Persistence Ignorance | Domain models have no persistence annotations | domain/model/ |
Best Practices
Keep Domain Pure
Keep Domain Pure
Domain models should have no dependencies on frameworks or infrastructure. They’re just plain Java.
Validate at Construction
Validate at Construction
Use constructors and factory methods to ensure objects are always in a valid state.
Favor Immutability
Favor Immutability
Make domain objects immutable where possible. Return new instances instead of modifying state.
Express Business Rules
Express Business Rules
Business rules should be explicit in domain code, not hidden in service layers or database triggers.
Use Value Objects Liberally
Use Value Objects Liberally
Wrap primitive types in value objects for type safety and validation.
Next Steps
Reactive Programming
Learn how domain operations are made reactive
Hexagonal Architecture
See how DDD integrates with ports and adapters