Learn to build a Modular Monolith by structuring your Laravel directory by domain, enforcing encapsulation, and defining public interfaces for module communication.
Previously in this course, we explored Defining Bounded Contexts: Architecting for Scale in Laravel. We established the conceptual boundaries for our SaaS; now, we must physically reflect those boundaries in our file system.
In a standard Laravel installation, as discussed in Installing Laravel and Exploring Directory Structure, the app/ directory is organized by technical concern (Controllers, Models, Jobs). This leads to the "God Object" anti-pattern in large systems. A Modular Monolith shifts this paradigm, organizing code by domain rather than by file type.
To achieve a true Modular Monolith, we move away from the flat app/ structure. Instead, we create a src/ or modules/ directory where each folder represents a Bounded Context.
Inside each module, you mirror the MVC structure or, more ideally, the clean architecture pattern we've been building upon.
TEXTapp/ modules/ Billing/ Actions/ Contracts/ DTOs/ Models/ Providers/ Services/ routes.php Identity/ Actions/ Models/ Services/ ...
By grouping these files, you ensure that a developer working on the Billing module rarely needs to touch the Identity module. This physical isolation drastically reduces cognitive load and prevents "spaghetti" dependencies.
The greatest risk in a monolith is tight coupling. If your Billing controller directly calls Identity\Models\User::class and manipulates its data, you've created a hidden dependency. When you eventually need to change the User schema, you break Billing silently.
Encapsulation is achieved by defining a "Public API" for each module. Nothing outside the module should touch its internal classes (like Models or internal Services) directly.
Let’s define a Billing module that exposes a single entry point for the Identity module to use.
1. The Internal Service (Private)
PHPnamespace Modules\Billing\Services; class SubscriptionManager { #6A9955">// This class should not be injected directly into other modules public function createSubscription(int $userId, string $planId): void { ... } }
2. The Public Facade/Interface (Public)
Create a Contract or a Service Provider that exposes only what is necessary.
PHPnamespace Modules\Billing; use Modules\Billing\Services\SubscriptionManager; class BillingGateway { public function __construct(private SubscriptionManager $manager) {} public function subscribeUser(int $userId, string $planId): void { $this->manager->createSubscription($userId, $planId); } }
3. Registration via Service Provider
Register this gateway in your module's ServiceProvider, ensuring the rest of the application resolves the BillingGateway rather than the SubscriptionManager.
For our running project, identify one core domain (e.g., "Subscriptions").
modules/Subscriptions directory.Subscription model, related Actions, and Services into this new folder.SubscriptionFacade class that acts as the only entry point for other modules.SubscriptionFacade instead of injecting the Subscription model directly.Shared or Common directory for reused code. This quickly becomes a dumping ground for "everything," effectively recreating the monolithic mess you're trying to escape. Keep Shared strictly for truly domain-agnostic utilities (e.g., custom Value Objects or logging helpers).Modular Monolith architecture isn't just about moving folders; it's about enforcing boundaries. By grouping by domain and strictly controlling access via public interfaces, you create a codebase that is easier to reason about, test, and—if the day comes—extract into a microservice. As explored in Project Structure for Large Applications: Domain-Driven Laravel, this structural clarity is the foundation for long-term scalability.
Up next: We will dive into Querying with Strict Eloquent to ensure our domain boundaries are enforced at the database level.
Master contract testing in Laravel to ensure your decoupled modules stay compatible. Learn to implement consumer-driven contracts to prevent breaking changes.
Read moreMaster Testing DDD components in Laravel. Learn to mock external services, isolate domain logic, and write reliable PHPUnit tests for your Action classes.
Modular Monolith Structure
Custom Middleware Development
Database Connection Pooling
Handling Large Data Exports
Security Header Configuration
Database Sharding Concepts
Real-time Data Synchronization
Database Deadlock Prevention
Managing Third-Party API Integrations