Modern PHP Architecture: DDD, CQRS, and Testing in 2026
How PHP 8.3+ and Laravel make domain-driven design, CQRS, and solid testing practical, with real examples.
PHP is no longer "just a scripting language." With PHP 8.3+ (typed class constants, readonly properties, attributes), the language supports patterns that used to feel clumsy. I have been building Laravel apps with DDD, CQRS, and a proper testing pyramid for years. Here is what works in 2026.
PHP in 2026: Fewer Excuses
The ecosystem has matured. You have typed properties, enums, match expressions, and attributes. You can model domain concepts without fighting the type system. Value objects, command handlers, and event listeners all fit naturally.
When someone says "PHP is messy," they often mean PHP 5. That world is gone. Modern PHP gives you enough structure to build large applications without the boilerplate of Java or the complexity of some enterprise frameworks.
Domain-Driven Design in Laravel
DDD is not just for Java. Laravel's service container, dependency injection, and PHP's type system make rich domain models practical.
Value objects are first-class. A Money type that enforces currency consistency:
1readonly class Money2{3 public function __construct(4 private int $amount,5 private Currency $currency,6 ) {}78 public function add(Money $other): self9 {10 if (!$this->currency->equals($other->currency)) {11 throw new CurrencyMismatchException();12 }1314 return new self($this->amount + $other->amount, $this->currency);15 }16}17No accidental mixing of euros and dollars. The type system enforces it. You can take this further with small, immutable types that make invalid states unrepresentable.
In Laravel, you keep domain logic in Domain classes and use the framework for routing, storage, and queues. Controllers stay thin. They delegate to application services, which orchestrate domain objects. The domain does not know about Laravel.
Event-Driven Architecture
Moving from synchronous request-response to events lets parts of the system scale independently. A user signs up? Emit UserRegistered. A listener sends welcome email. Another syncs to a CRM. A third updates analytics. Add more listeners without touching the signup flow.
Laravel's event system is simple. event(new UserRegistered($user)). Listeners can be sync or queued. For heavy work, queue them. For audit logs or cache invalidation, run sync. The pattern is the same.
Event sourcing is a step further: store events, not state. Rebuild state by replaying. Useful for compliance, audit trails, or when you need to answer "what happened?" Not every app needs it. Start with events. Add event sourcing if the domain demands it.
CQRS for Complex Domains
Command Query Responsibility Segregation separates reads from writes. Commands change state. Queries return data. They can use different models, different databases, different optimizations.
In admin panels, read patterns differ from write patterns. You might list orders with filters, sort, and paginate. That is a query. The write path: create order, add line items, apply discount. Different shapes. CQRS lets you optimize each.
For read-heavy screens, you might use a denormalized view or a separate read model. For writes, you go through a command handler that validates and persists. Laravel does not enforce CQRS, but it does not block it. Commands, handlers, and repositories fit cleanly.
The Testing Pyramid
Modern PHP projects need a balanced strategy. Unit tests for domain logic. Integration tests for infrastructure. Feature tests for user flows.
Domain logic: pure. No database, no HTTP. Test Money::add(), Order::applyDiscount(), validation rules. Fast. Hundreds of tests in seconds.
Infrastructure: hit the database, Redis, external APIs. Test repositories, queued jobs, migrations. Slower but still manageable.
Feature tests: full HTTP requests. Test the full stack. Critical paths only. A few dozen at most.
Tools like Pest PHP make this enjoyable. Descriptive test names, readable output, and minimal boilerplate. I run unit tests on every save. Integration and feature tests before commit.
Choosing Patterns That Fit
The biggest mistake: applying patterns because they are popular. DDD is great for complex domains with lots of rules. For a CRUD app, it might be overkill. CQRS helps when reads and writes really diverge. Simple apps might not need it.
Start with the problem. What is complex? What changes often? What are the performance requirements? Then choose patterns that address those. Laravel gives you enough structure for most cases. Add DDD or CQRS when the domain justifies it.
Conclusion
PHP powers some of the most complex web applications in production. The key is choosing patterns that match your domain complexity. With PHP 8.3+ and Laravel, you have the tools. Use them when they help. Skip them when they do not.