Defining Bounded Contexts: Architecting for Scale in Laravel
Learn to apply Bounded Contexts to your Laravel application. Stop the "god object" anti-pattern and isolate domain logic to improve scalability and maintainability.
Previously in this course, we discussed Transitioning from MVC to DDD: Scaling Your Laravel Architecture, where we explored the shift from monolithic controllers to domain-centric design. In this lesson, we take the next step by explicitly defining Bounded Contexts to segment our SaaS platform into manageable, isolated subdomains.
The Problem: When Everything is Everywhere
In a standard Laravel application, the User model often becomes a "God Object." It handles authentication, profile management, billing status, subscription checks, and notification preferences. As your SaaS grows, this single file becomes a bottleneck. Any change to the billing logic risks breaking the authentication flow, and testing becomes a nightmare of cascading dependencies.
A Bounded Context is the solution. It is the specific perimeter within which a particular domain model is defined and applicable. By isolating these contexts, we ensure that the "User" in the Billing context is not the same "User" in the Identity (Auth) context.
Mapping Subdomains
Before writing code, we must map our business requirements into subdomains. For our SaaS project, we classify these as:
- Core Domain: The unique logic that gives our SaaS its competitive advantage (e.g., the proprietary data processing engine).
- Supporting Subdomain: Necessary, but not unique (e.g., User Management).
- Generic Subdomain: Common to every SaaS (e.g., Billing/Payments).
Domain Boundary Table
| Context | Responsibility | Key Entities |
|---|---|---|
| Identity | Auth, Profile, Roles | User, Session, Permission |
| Billing | Subscriptions, Invoices | Subscription, Plan, PaymentMethod |
| Catalog | Product Listings, Pricing | Product, Price, Category |
Defining Boundaries: Users vs. Billing
We often make the mistake of creating a Subscription model that belongs to the User model directly. In a DDD approach, we decouple these. The Billing context doesn't need to know about the user's password or email preferences; it only needs an identifier to link a subscription to an entity.
Worked Example: Decoupled Contexts
Let's look at how we prevent the User model from knowing about Subscription logic.
PHP#6A9955">// src/Domains/Billing/Models/Subscription.php namespace App\Domains\Billing\Models; use Illuminate\Database\Eloquent\Model; class Subscription extends Model { #6A9955">// The Billing context only cares about the 'owner_id' #6A9955">// It is agnostic to the Users table internals protected $fillable = ['owner_id', 'status', 'plan_id']; }
Instead of a User->subscriptions() relationship, we use a Service or an Action to bridge the gap.
PHP#6A9955">// src/Domains/Billing/Actions/GetSubscriptionStatus.php namespace App\Domains\Billing\Actions; use App\Domains\Billing\Models\Subscription; class GetSubscriptionStatus { public function execute(int $userId): string { return Subscription::where('owner_id', $userId)->first()?->status ?? 'inactive'; } }
By using an owner_id integer rather than an Eloquent relationship, we have created a hard boundary. If we decide to move Billing to a separate microservice later, we only need to change the implementation of this Action, not the entire codebase.
Hands-on Exercise
- Identify one "leaky" relationship in your current project (e.g., a
Usermodel that callsBillingmethods). - Create a new directory structure:
app/Domains/Identityandapp/Domains/Billing. - Move your relevant models into these directories.
- Replace the direct Eloquent relationship with a simple ID-based look-up service or action as shown above.
Common Pitfalls
- Over-Engineering: Do not create separate namespaces for every tiny feature. Start with high-level domains (Billing, Identity, Catalog) and only split further if the complexity warrants it.
- Shared Models: Never share a model across boundaries. If both Identity and Billing need a
User, create aUsermodel in Identity and aCustomerrepresentation in Billing that holds only the fields necessary for payments. - Database Coupling: While you might share a database initially, try to keep tables prefixed or strictly separated by domain so that migrating to a separate DB later is a matter of configuration rather than a massive refactor.
Recap
We’ve learned that a Bounded Context is not just a folder structure; it is a conceptual boundary that prevents domain knowledge from bleeding across the system. By isolating the Billing context from the Identity context, we reduce cognitive load and prepare our Laravel SaaS for modular scaling.
Up next: Implementing Action Classes — where we replace bloated controllers with single-purpose domain actions.
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.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.