| name | implementing-ddd-architecture |
| description | Design and implement DDD patterns (entities, value objects, aggregates, CQRS). Use when creating new domain objects, implementing bounded contexts, designing repository interfaces, or learning proper layer separation. For fixing existing Deptrac violations, use the deptrac-fixer skill instead. |
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 deptrac shows 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.
Architecture Planning: Existing Structure First
When writing architecture specs or proposing new classes for this repository, inspect the current source tree and deptrac.yaml before inventing directories.
Required workflow:
- List existing directories for the bounded context, for example
find src/Core/Customer -maxdepth 4 -type d | sort.
- Check
deptrac.yaml collectors for allowed Application, Domain, and Infrastructure directory names.
- Reuse existing type directories whenever they already express the class responsibility.
- Keep the rule one directory = one class type. Do not place policies, schedulers, registries, handlers, DTOs, and resolvers together in a broad feature bucket.
- If a feature already exists through
Repository, Collection, Resolver, EventSubscriber, or other established directories, extend that feature surface instead of creating a new umbrella directory such as Cache.
- Introduce a new directory type only when no existing type fits, it is added to deptrac, and the architecture spec explains why the new type is necessary.
For CQRS in this codebase, prefer:
Application/Command/{Action}{Entity}Command.php
Application/CommandHandler/{Action}{Entity}CommandHandler.php
Do not propose ReadModel, Query, QueryHandler, Message, MessageHandler, Policy, Registry, or Scheduler directories unless the current repo already uses that directory type and deptrac collects it. Reads in the current Customer context are served by repositories, resolvers, processors, and DTOs rather than a separate read-model directory.
Example for Customer cache planning:
- Shared reusable refresh work:
Shared/Application/Command/CacheRefreshCommand.php
- Shared reusable worker:
Shared/Application/CommandHandler/CacheRefreshCommandHandler.php
- Shared reusable handler base:
Shared/Application/CommandHandler/AbstractCacheRefreshCommandHandler.php
- Shared reusable metrics:
Shared/Application/Observability/Metric/CacheRefreshSucceededMetric.php
- Customer adapter execution:
Core/Customer/Application/CommandHandler/CustomerCacheRefreshCommandHandler.php
- Customer adapter factory:
Core/Customer/Application/Factory/CustomerCacheRefreshCommandFactory.php
- Customer policy lookup:
Core/Customer/Infrastructure/Collection/CustomerCachePolicyCollection.php plus Core/Customer/Infrastructure/Resolver/CustomerCachePolicyResolver.php
- Customer cached storage access: existing
Core/Customer/Infrastructure/Repository/CachedCustomerRepository.php
When a feature must be reusable across bounded contexts, put the generic command, worker, abstract base classes, DTOs, resolver interfaces, and metrics in Shared first. Keep each bounded context as a thin adapter that maps its domain events, tags, targets, policies, and repository warmup behavior into the shared contract. Do not route Customer-specific command payloads if the same queue and worker must later refresh other domains.
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, MongoDB |
See: DIRECTORY-STRUCTURE.md for complete file placement guide.
Critical Rules
0. No Static Methods in Production Source
Do not add or keep static method declarations in project PHP files. Model behavior as injected services, listeners, factories, or regular value objects so dependencies remain explicit and testable.
- HTTP request preconditions belong in kernel listeners, not static guards inside processors
- Object creation belongs in factory services
- Shared helpers should be injectable services
- Run
make forbid-static-methods or make psalm before finishing
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;
}
}
✅ GOOD (Rich):
class Customer {
public function changeName(CustomerName $name): void {
$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]
private string $name;
}
✅ GOOD: Validation in YAML config (Preferred)
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)
final readonly class CreateCustomerCommand implements CommandInterface
{
public function __construct(
public string $id,
public string $name,
public string $email
) {}
}
Command Handlers
final readonly class CreateCustomerCommandHandler implements CommandHandlerInterface
{
public function __invoke(CreateCustomerCommand $command): Customer
{
$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)
interface CustomerRepositoryInterface
{
public function save(Customer $customer): void;
public function findById(string $id): ?Customer;
}
Implementation (Infrastructure Layer)
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
final readonly class CustomerNameChangedSubscriber implements DomainEventSubscriberInterface
{
public function __invoke(CustomerNameChanged $event): void
{
}
}
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 deptrac shows 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
NEVER
- Add framework imports to Domain layer
- Put business logic in Application handlers
- Create anemic domain models (getters/setters only)
- Modify
deptrac.yaml to allow violations
- Skip validation (either in Value Objects or YAML config)
- Use public setters in entities
- Create a new feature bucket directory when existing class-type directories fit
- Propose directories that deptrac does not collect without explicitly updating and validating deptrac
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
- Reuse existing bounded-context directory names before adding new ones
- Keep one directory focused on one class type
- Verify with
make deptrac after 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:
Related Skills
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
class CreateCustomerHandler {
public function __invoke($command) {
if (strlen($command->name) < 2) {
throw new Exception();
}
}
}
❌ Framework Dependencies in Domain
use Symfony\Component\Validator\Constraints as Assert;
class Customer {
#[Assert\NotBlank]
private string $name;
}
❌ Anemic Domain Models
class Customer {
public function setName(string $name): void {
$this->name = $name;
}
}
✅ 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.