Transitioning from MVC to DDD: Scaling Your Laravel Architecture
Move beyond "Fat Controllers" and embrace Domain-Driven Design (DDD). Learn how to identify business boundaries and scale your Laravel SaaS architecture.
Previously in this course, we discussed the foundational patterns for building clean applications. In this lesson, we shift our perspective from the standard Model-View-Controller (MVC) pattern to Domain-Driven Design (DDD). This transition is the primary catalyst for scaling a high-traffic SaaS platform without drowning in technical debt.
MVC vs. DDD: The Conceptual Shift
In standard Laravel development, we often organize by technical role: app/Models, app/Http/Controllers, app/Jobs. This is fine for small projects, but as your SaaS grows, your UserController becomes a dumping ground for logic related to authentication, billing, team management, and notification preferences. You end up with "Fat Controllers" that are impossible to test in isolation.
MVC focuses on the request/response cycle. DDD focuses on the business problem.
In DDD, we organize by domain capability. Instead of a UserController that handles everything a user does, we define bounded contexts like Billing, Identity, and Collaboration. The code lives where the business logic resides, not where the framework tells us to put it.
Identifying Domain Boundaries
Before refactoring your SaaS, you must identify your domains. A domain is a specific sphere of knowledge or activity. If you look at your User model and see methods like charge(), inviteTeamMember(), and generateReport(), you have a boundary violation.
To identify boundaries in your existing codebase, map your features to these categories:
- Core Domain: The unique value proposition of your SaaS (e.g., the complex algorithm that powers your analytics dashboard).
- Supporting Subdomains: Features that are necessary but not unique (e.g., user management, notification delivery).
- Generic Subdomains: Commodity features (e.g., authentication, payments via Stripe).
Worked Example: From Controller to Domain
Imagine a standard SubscriptionController. It currently handles Stripe webhooks, database updates, and email notifications.
The MVC "Fat" approach:
PHPpublic function store(Request $request) { $user = auth()->user(); #6A9955">// Logic for Stripe, Database, and Email mixed together $stripe = new StripeClient(config('services.stripe.key')); $stripe->subscriptions->create([...]); $user->update(['status' => 'active']); Mail::to($user)->send(new WelcomeEmail()); }
The DDD "Domain" approach:
We extract this into a Billing domain. We create an Action class that handles the domain logic, independent of the HTTP layer. As we explored in Architecting for Maintainability: Refactoring Laravel Controllers, isolating this logic allows us to reuse it in CLI commands or queued jobs without duplicating code.
PHP#6A9955">// app/Domains/Billing/Actions/CreateSubscriptionAction.php public function execute(User $user, array $data): Subscription { #6A9955">// Domain logic only $stripeSubscription = $this->stripeGateway->create($user, $data); return $user->subscriptions()->create([...]); }
By shifting the logic here, the controller becomes a thin transport layer. It only parses the request and calls the Domain Action.
Hands-on Exercise
- Open your current SaaS project.
- Identify three distinct "activities" your users perform (e.g., "Updating Profile," "Processing Payment," "Generating Report").
- List the models involved in each. If a model appears in all three, you have identified a high-traffic intersection that needs decoupling.
- Draft a directory structure on paper where these activities live in
app/Domains/{Name}/Actionsrather than grouped by file type.
Common Pitfalls
- Over-Engineering: Don't create a complex "Bounded Context" for a simple CRUD blog. DDD is for high-complexity systems.
- Anemic Domains: Don't just move logic to a
Serviceclass and leave your models empty. Your Eloquent models should still encapsulate state-related logic (e.g.,isExpired()), while complex business processes belong in Actions or Services. - Ignoring the Language: DDD is about "Ubiquitous Language." If your code calls it a
Subscriptionbut the business calls it aMembership, rename your classes to match the business language.
Recap
Transitioning to DDD isn't about ditching Laravel's MVC; it's about evolving how you organize your application logic. By moving from technical groupings to business domains, you create a codebase that is easier to test, maintain, and scale. We've started this shift by identifying domain boundaries, a concept we'll deepen as we look at Implementing the Service Layer in Laravel for Maintainable Code.
Up next: Defining Bounded Contexts — we will map out subdomains and define clear boundaries for Users and Billing.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

Laravel SaaS MVP & Multi-Tenant App Development
Launch your SaaS MVP on Laravel — multi-tenant, subscription-ready, and built by the engineer behind a platform serving 10,000+ paying users.