Omar Bahra
All posts
Laravel DDD Architecture

What DDD in Laravel Actually Looks Like After Three Products

December 5, 20242 min readby MHD Omar Bahra

Domain-Driven Design attracts two crowds: people who read the blue book and want the full ceremony, and people who think it's all overengineering. After building three production Laravel systems on a DDD-ish layout — an LMS, an ambassador platform, and an AI communications platform — I've landed somewhere practical in between.

What survived

Bounded contexts as top-level folders. src/Domain/Billing, src/Domain/Scheduling, src/Domain/Conversations. When a new developer asks "where does the subscription logic live?", the answer is a folder name. This alone justifies the layout.

Actions. Small classes that do one business thing: UpgradeOrDowngradeUserSubscriptionAction, TrackFeatureUsageAction. They're trivially testable, they read like the requirements doc, and they end the "which service class does this belong in?" debate because the answer is always "a new one."

Domain events for side effects. A badge system, a billing meter and a notification flow all listening to events instead of being called inline. When product says "also send an email when X happens", that's a listener, not surgery.

DTOs at the boundaries. Request data becomes a typed object before it touches the domain. Half the bugs in loose Laravel apps are arrays whose shape everyone remembers slightly differently.

What I dropped

Repositories over Eloquent. Wrapping every model in a repository interface, for the flexibility of swapping an ORM I will never swap — that's ceremony without payoff. Eloquent is the persistence layer; I stopped pretending otherwise.

Value objects for everything. A Money object earns its keep. A FirstName object does not.

Aggregate purity debates. In a codebase with deadlines, "does this reference cross an aggregate boundary?" matters far less than "can a new developer find and change this safely?"

The honest summary

The point was never the vocabulary. The point is that at 100K lines, the system still has places where things go — and version 748 of the codebase is as navigable as version 48. That's the entire pitch.

Enjoyed this post?

Subscribe to the newsletter

Get future posts delivered to your inbox. No spam, unsubscribe anytime.