The software industry's knee-jerk reaction to scaling pain has long been distributed microservices. However, splitting a domain into dozens of network-isolated services too early introduces distributed consensus challenges, network latency, complex debugging, and operational overhead that drain engineering velocity.
For many high-growth SaaS platforms, the sweet spot lies in the Modular Monolith. This architectural pattern enforces strict domain boundaries, isolated business logic, and explicit inter-module communication within a single deployable artifact.
Laravel 11 provides a streamlined core foundation—featuring a trimmed-down default directory structure, native asynchronous job batching, and robust event dispatching—making it an ideal runtime for a strict modular monolith.
Deconstructing the Modular Monolith
A modular monolith divides your application codebase into autonomous domain modules (e.g., Billing, Identity, Catalog, Shipping). Each module acts as a mini-application containing its own controllers, models, migrations, service classes, and event listeners.
Crucially, modules must not directly query foreign database tables or instantiate foreign Eloquent models. Cross-module communication occurs exclusively through explicit public APIs, contracts, or asynchronous domain events.
┌────────────────────────────────────────────────────────┐
│ Laravel 11 Core │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Identity Module │ │ Billing Module │ │
│ │ - Controllers │ │ - Controllers │ │
│ │ - Models │ │ - Models │ │
│ │ - Service Contracts │◄───┤ - Service Contracts │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │ │ │
│ └────────► [ Events ] ◄─────┘ │
└────────────────────────────────────────────────────────┘
Architectural Comparison: Monolith vs. Modular Monolith vs. Microservices
| Metric | Traditional Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deployment Unit | Single Artifact | Single Artifact | Multiple Independent Services |
| Domain Boundary | Soft (Spaghetti dependencies) | Strict (Enforced via Contracts) | Strict (Network-enforced) |
| Transaction Integrity | Native ACID across tables | Native ACID within module, Eventual consistency across | Distributed transactions (Saga pattern required) |
| Operational Overhead | Low | Low-Medium | High (Service mesh, K8s, tracing) |
| Refactoring Cost | Exponentially High | Low (Contained within module) | High (API contract versioning) |
Directory Structure and Namespace Isolation
To enforce modularity in Laravel 11, we shift domain code out of the default app/ directory and into a dedicated modules/ root folder. Each module maintains its own Laravel service provider to register routes, configuration, and database migrations.
app/
├── Http/
├── Providers/
modules/
├── Billing/
│ ├── src/
│ │ ├── Contracts/
│ │ │ └── BillingGatewayInterface.php
│ │ ├── Http/
│ │ │ └── Controllers/
│ │ │ └── CheckoutController.php
│ │ ├── Models/
│ │ │ └── Invoice.php
│ │ ├── Services/
│ │ │ └── StripeGateway.php
│ │ ├── Providers/
│ │ │ └── BillingServiceProvider.php
│ │ └── Database/
│ │ └── migrations/
│ └── composer.json
└── Identity/
└── src/
└── ...
Configuring Autoloading via Path Repositories
To allow Composer to discover our modules without publishing them to Packagist, define them as path repositories in the project's root composer.json:
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"repositories": [
{
"type": "path",
"url": "modules/*"
}
]
}
Each module manages its own dependencies and autoloading namespaces inside its own modules/Billing/composer.json:
{
"name": "bricktry/billing",
"autoload": {
"psr-4": {
"Modules\\Billing\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Modules\\Billing\\Providers\\BillingServiceProvider"
]
}
}
}
Implementing Domain Contracts and Decoupled Events
When the Identity module needs to trigger an action in the Billing module (e.g., creating a customer record upon user registration), direct dependency injection of Modules\Billing\Models\Customer into Identity code creates tight coupling.
Instead, rely on PHP interfaces bound in service containers and decoupled domain events.
1. Defining the Service Contract
Create an interface inside the consuming or producing module's contract namespace:
namespace Modules\Billing\Contracts;
interface BillingManagerInterface
{
public function createCustomerForUser(int $userId, string $email): string;
public function charge(string $customerId, int $amountCents): bool;
}
2. Registering Bindings in the Module Service Provider
Inside BillingServiceProvider, bind the concrete implementation to the container:
namespace Modules\Billing\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Billing\Contracts\BillingManagerInterface;
use Modules\Billing\Services\StripeBillingManager;
class BillingServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(BillingManagerInterface::class, StripeBillingManager::class);
}
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../Database/migrations');
$this->loadRoutesFrom(__DIR__ . '/../Http/routes.php');
}
}
3. Cross-Module Communication via Domain Events
For asynchronous workflows, dispatch native Laravel events from one domain and listen for them in another. For instance, when a user updates their subscription plan, dispatch a domain event:
namespace Modules\Billing\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class SubscriptionUpgraded
{
use Dispatchable, SerializesModels;
public function __construct(
public readonly int $userId,
public readonly string $newTier
) {}
}
The Analytics or Notification module can then listen for SubscriptionUpgraded without the Billing module knowing about its existence:
namespace Modules\Notifications\Listeners;
use Modules\Billing\Events\SubscriptionUpgraded;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendUpgradeConfirmationEmail implements ShouldQueue
{
public function handle(SubscriptionUpgraded $event): void
{
// Dispatch transactional email logic safely isolated here
}
}
How BrickTry Accelerates & Powers This
Architecting, scaffolding, and maintaining a strict modular monolith requires immense discipline. Refactoring legacy controllers, writing precise composer configurations, and maintaining interface contracts across multiple sub-packages can introduce friction.
BrickTry streamlines this engineering workflow through a unified developer experience:
- BrickTry Lab Sandbox (
/lab): Spin up an instant, zero-setup in-browser virtualized container runtime to prototype module boundaries, test service container bindings, and run PHPUnit test suites without local environment configuration drift. - AI-Human Dev Pairing: Leverage autonomous AI agents to scaffold module directory trees, generate PSR-4
composer.jsonmanifests, and draft interface contracts, while dedicated senior full-stack engineering pods review your domain boundaries and database constraints. - Interactive Scoping Engine: Translate high-level product requirements into granular architectural milestones, database schema definitions, and module dependency graphs automatically.
- Unified Importer: Seamlessly import existing monolithic Laravel repositories, automatically parsing tangled controllers and mapping them into isolated domain modules with 1-click refactoring suggestions.
- 100% Source Code Ownership: Retain complete, unencumbered ownership of your GitHub repositories, Dockerfiles, and database migrations with zero vendor lock-in.
Conclusion
Adopting a modular monolith in Laravel 11 allows engineering teams to enjoy the development velocity of a monolith combined with the architectural clarity and testability of microservices. By enforcing strict boundary contracts, utilizing PSR-4 path repositories, and relying on event-driven decoupling, you ensure your codebase remains maintainable as your engineering organization and product feature set scale.
Build, Test, and Scale This on BrickTry
BrickTry pairs you with autonomous AI scaffolding supervised by dedicated senior full-stack software engineers in an interactive in-browser development sandbox. Test, build, and deploy production-grade software with 100% source code ownership and zero vendor lock-in.