Implementing DDD Architecture
Context (Input)
- •Creating new entities, value objects, or aggregates
- •Implementing bounded contexts or modules
- •Designing repository interfaces and implementations
- •Learning proper layer separation (Domain/Application/Infrastructure)
- •Need to understand CQRS pattern (Commands, Handlers, Events)
- •Code review for architectural compliance
Task (Function)
Design and implement rich domain models following DDD, hexagonal architecture, and CQRS patterns.
Success Criteria:
- •Domain entities remain framework-agnostic (no framework imports)
- •Business logic in Domain layer, not in Application handlers
- •
make deptracshows zero violations - •Repository interfaces in Domain, implementations in Infrastructure
Core Principle
Rich Domain Models, Not Anemic
Business logic belongs in the Domain layer. Application layer orchestrates, Domain executes.
Layer Dependency Rules
Domain ─────────────────> (NO dependencies - pure PHP)
│
│
Application ──────────> Domain + Infrastructure
│
│
Infrastructure ───────> Domain + Application
Allowed Dependencies:
| Layer | Can Import |
|---|---|
| Domain | ❌ Nothing (pure PHP, SPL, domain-specific libraries only) |
| Application | ✅ Domain, Infrastructure, Symfony, API Platform |
| Infrastructure | ✅ Domain, Application, Symfony, Doctrine ORM |
Template examples may show Doctrine ODM/MongoDB constructs. In this service, use Doctrine ORM with MySQL (
EntityManagerInterface,.orm.xmlmappings).
See: DIRECTORY-STRUCTURE.md for complete file placement guide.
Critical Rules
1. Domain Layer Purity
❌ FORBIDDEN in Domain:
- •Symfony components (
use Symfony\...) - •Doctrine annotations/attributes
- •API Platform attributes
- •Any framework-specific code
✅ ALLOWED in Domain:
- •Pure PHP
- •SPL (Standard PHP Library)
- •Domain-specific value objects
- •Domain interfaces
2. Rich Domain Models
❌ BAD (Anemic):
class Customer {
public function setName(string $name): void {
$this->name = $name; // No validation!
}
}
✅ GOOD (Rich):
class Customer {
public function changeName(CustomerName $name): void {
// Business rules enforced
$this->record(new CustomerNameChanged($this->id, $name));
$this->name = $name;
}
}
3. Validation Pattern
❌ BAD: Validation in Domain with Symfony
use Symfony\Component\Validator\Constraints as Assert;
class Customer {
#[Assert\NotBlank] // ❌ Framework in Domain!
private string $name;
}
✅ GOOD: Validation in YAML config (Preferred)
# config/validator/Customer.yaml
App\Application\DTO\CustomerCreate:
properties:
name:
- NotBlank: ~
- Length:
min: 2
max: 100
Framework validators should always be used when possible. They provide:
- •Centralized configuration
- •Easy maintenance
- •Standard error messages
- •Built-in constraints (NotBlank, Email, Length, etc.)
- •Custom validators for business rules
Value Objects should only be used when:
- •Framework validators cannot express the business rule
- •Complex domain logic requires encapsulation
- •The validation is part of domain invariants
See: REFERENCE.md for complete validation patterns.
CQRS Pattern Quick Start
Commands (Write Operations)
// src/Core/{Context}/Application/Command/{Action}{Entity}Command.php
final readonly class CreateCustomerCommand implements CommandInterface
{
public function __construct(
public string $id,
public string $name,
public string $email
) {}
}
Command Handlers
// src/Core/{Context}/Application/CommandHandler/{Action}{Entity}CommandHandler.php
final readonly class CreateCustomerCommandHandler implements CommandHandlerInterface
{
public function __invoke(CreateCustomerCommand $command): Customer
{
// Minimal orchestration only
$customer = Customer::create(
Ulid::fromString($command->id),
new CustomerName($command->name),
new Email($command->email)
);
$this->repository->save($customer);
$this->eventBus->publish(...$customer->pullDomainEvents());
return $customer;
}
}
See: REFERENCE.md for complete CQRS patterns.
Repository Pattern
Interface (Domain Layer)
// src/Core/{Context}/Domain/Repository/{Entity}RepositoryInterface.php
interface CustomerRepositoryInterface
{
public function save(Customer $customer): void;
public function findById(string $id): ?Customer;
}
Implementation (Infrastructure Layer)
// src/Core/{Context}/Infrastructure/Repository/{Entity}Repository.php
final class CustomerRepository implements CustomerRepositoryInterface
{
public function __construct(
private readonly DocumentManager $documentManager
) {}
public function save(Customer $customer): void
{
$this->documentManager->persist($customer);
$this->documentManager->flush();
}
}
Register in config/services.yaml:
App\Core\Customer\Domain\Repository\CustomerRepositoryInterface: alias: App\Core\Customer\Infrastructure\Repository\CustomerRepository
Domain Events Pattern
Recording Events in Aggregates
class Customer extends AggregateRoot // Provides event recording
{
public function changeName(CustomerName $name): void
{
$this->name = $name;
$this->record(new CustomerNameChanged($this->id, $name));
}
}
Event Subscribers
// src/Core/{Context}/Application/EventSubscriber/{Event}Subscriber.php
final readonly class CustomerNameChangedSubscriber implements DomainEventSubscriberInterface
{
public function __invoke(CustomerNameChanged $event): void
{
// React to event (e.g., send notification)
}
}
See: REFERENCE.md for complete event-driven patterns.
Quick Start Workflows
Creating a New Entity
- •Create Entity in
Domain/Entity/ - •Create Value Objects in
Domain/ValueObject/ - •Create Repository Interface in
Domain/Repository/ - •Create Repository Implementation in
Infrastructure/Repository/ - •Create Commands in
Application/Command/ - •Create Handlers in
Application/CommandHandler/ - •Verify:
make deptracshows zero violations
See: examples/ for complete working examples.
Fixing Deptrac Violations
If make deptrac shows violations:
Use: deptrac-fixer skill for step-by-step fix patterns.
Constraints (Parameters)
NEVER
- •Add framework imports to Domain layer
- •Put business logic in Application handlers
- •Create anemic domain models (getters/setters only)
- •Modify
deptrac.yamlto allow violations - •Skip validation (either in Value Objects or YAML config)
- •Use public setters in entities
ALWAYS
- •Keep Domain layer pure (no framework dependencies)
- •Put business logic in Domain entities/aggregates
- •Use Value Objects for validation and invariants
- •Create repository interfaces in Domain layer
- •Implement repositories in Infrastructure layer
- •Use Command Bus for write operations
- •Record Domain Events for state changes
- •Verify with
make deptracafter changes
Format (Output)
Expected Directory Structure
src/Core/{Context}/
├── Domain/
│ ├── Entity/
│ │ └── {Entity}.php # Pure PHP, no attributes
│ ├── ValueObject/
│ │ └── {ValueObject}.php # Validation logic here
│ ├── Repository/
│ │ └── {Entity}RepositoryInterface.php
│ ├── Event/
│ │ └── {Event}.php
│ └── Exception/
│ └── {Exception}.php
├── Application/
│ ├── Command/
│ │ └── {Action}{Entity}Command.php
│ ├── CommandHandler/
│ │ └── {Action}{Entity}CommandHandler.php
│ └── EventSubscriber/
│ └── {Event}Subscriber.php
└── Infrastructure/
└── Repository/
└── {Entity}Repository.php
Expected Deptrac Output
✅ No violations found
Verification Checklist
After implementing DDD patterns:
- • Domain entities have no framework imports
- • Business logic in Domain layer, not Application
- • Value Objects used for validation and invariants
- • Repository interfaces in Domain layer
- • Repository implementations in Infrastructure layer
- • Commands implement
CommandInterface - • Handlers implement
CommandHandlerInterface - • Domain Events recorded in aggregates
- • Event Subscribers implement
DomainEventSubscriberInterface - •
make deptracshows zero violations - • All tests pass
- •
make cipasses
Related Skills
- •deptrac-fixer - Fix architectural violations
- •api-platform-crud - YAML-based API Platform with DDD
- •database-migrations - XML-based Doctrine mappings
- •complexity-management - Keep domain logic maintainable
Reference Documentation
For detailed patterns, workflows, and examples:
- •REFERENCE.md - Complete DDD workflows and patterns
- •DIRECTORY-STRUCTURE.md - File placement guide (CodelyTV style)
- •examples/ - Complete working examples:
- •Entity examples
- •Value Object examples
- •CQRS examples
- •Event-driven examples
Anti-Patterns to Avoid
❌ Business Logic in Handlers
// ❌ BAD: Logic in handler
class CreateCustomerHandler {
public function __invoke($command) {
if (strlen($command->name) < 2) { // ❌ Validation in handler!
throw new Exception();
}
// ...
}
}
❌ Framework Dependencies in Domain
// ❌ BAD: Symfony in Domain
use Symfony\Component\Validator\Constraints as Assert;
class Customer {
#[Assert\NotBlank] // ❌ Framework coupling!
private string $name;
}
❌ Anemic Domain Models
// ❌ BAD: Just getters/setters
class Customer {
public function setName(string $name): void {
$this->name = $name; // No business rules!
}
}
✅ GOOD Patterns
- •Value Objects enforce invariants
- •Domain methods express business operations
- •Handlers orchestrate, Domain executes
- •Configuration externalized to YAML/XML
CodelyTV Architecture Pattern
This project follows CodelyTV's hexagonal architecture patterns:
- •Directory structure: Bounded Context → Layer → Component Type
- •Naming conventions: Explicit suffixes (Command, Handler, Repository, etc.)
- •Layer isolation: Deptrac enforces boundaries
- •CQRS: Commands for writes, Queries for reads
- •Event-driven: Domain Events for decoupling
See: DIRECTORY-STRUCTURE.md for complete hierarchy.