Laravel Modular Monolith: Bounded Contexts Guide | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com) - [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [About](https://msaied.com#about)

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax

 Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax
====================================================================================

 Learn how to carve a Laravel application into cohesive bounded contexts using modules, internal contracts, and Pest arch tests — without the operational overhead of microservices.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com#person) Published 20 Oct 2026 · Updated 20 Oct 2026 · 3 min read

ShareCopy linkCopied

 ![Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax](https://cdn.msaied.com/799/f5bb5eaa2b223303a224d6094f2b562c.png) 

  On this page +1. [Why a Modular Monolith?](#why-a-modular-monolith)
2. [Directory Layout](#directory-layout)
3. [Defining Internal Contracts](#defining-internal-contracts)
4. [Communicating Across Contexts With Domain Events](#communicating-across-contexts-with-domain-events)
5. [Enforcing Boundaries With Pest Arch Tests](#enforcing-boundaries-with-pest-arch-tests)
6. [Shared Kernel: What Belongs There](#shared-kernel-what-belongs-there)
7. [Migration Strategy for Existing Apps](#migration-strategy-for-existing-apps)
8. [Takeaways](#takeaways)

 Why a Modular Monolith?
-----------------------

Microservices solve organisational scaling problems. Most teams don't have those problems yet — but they do have a tangled codebase where `Order` reaches directly into `Billing` and `Inventory` is a god-class that everyone is afraid to touch. A modular monolith gives you the domain isolation of services with a single deployment unit and zero network hops.

The goal is not to split files into folders. It is to enforce *contracts between contexts* so that each module can evolve independently.

Directory Layout
----------------

Place modules under `src/` (or `app/Modules/`) and register each as a PSR-4 root:

```php
src/
  Billing/
    BillingServiceProvider.php
    Application/        # use-cases / actions
    Domain/             # entities, value objects, domain events
    Infrastructure/     # Eloquent models, repositories, gateways
    Contracts/          # interfaces exposed to other modules
  Inventory/
    ...
  Ordering/
    ...

```

```php
// composer.json
"autoload": {
    "psr-4": {
        "App\\": "app/",
        "Billing\\": "src/Billing/",
        "Inventory\\": "src/Inventory/",
        "Ordering\\": "src/Ordering/"
    }
}

```

Each module ships its own `ServiceProvider` that registers routes, migrations, and bindings. Bootstrap them in `config/app.php` or via auto-discovery.

Defining Internal Contracts
---------------------------

The only surface one module may call on another is an interface living in the *caller's* `Contracts/` namespace. This is the dependency-inversion principle applied at the module boundary.

```php
// src/Ordering/Contracts/ReservesInventory.php
namespace Ordering\Contracts;

interface ReservesInventory
{
    public function reserve(string $sku, int $qty): ReservationId;
}

```

The `Inventory` module provides the concrete implementation and binds it in its own service provider:

```php
// src/Inventory/InventoryServiceProvider.php
$this->app->bind(
    \Ordering\Contracts\ReservesInventory::class,
    \Inventory\Infrastructure\EloquentInventoryReservation::class,
);

```

Now `Ordering` never imports an `Inventory` class directly — only the contract it owns.

Communicating Across Contexts With Domain Events
------------------------------------------------

For fire-and-forget side effects, dispatch a domain event instead of calling a contract:

```php
// src/Ordering/Domain/Events/OrderPlaced.php
namespace Ordering\Domain\Events;

use Ordering\Domain\ValueObjects\OrderId;

final readonly class OrderPlaced
{
    public function __construct(
        public OrderId $orderId,
        public \DateTimeImmutable $occurredAt,
    ) {}
}

```

```php
// src/Billing/Application/Listeners/ChargeCustomerOnOrderPlaced.php
namespace Billing\Application\Listeners;

use Ordering\Domain\Events\OrderPlaced;

final class ChargeCustomerOnOrderPlaced
{
    public function handle(OrderPlaced $event): void
    {
        // Billing reacts without Ordering knowing Billing exists
    }
}

```

Listeners are registered in `Billing`'s service provider. `Ordering` emits; `Billing` listens. The dependency arrow points in one direction only.

Enforcing Boundaries With Pest Arch Tests
-----------------------------------------

Conventions rot without enforcement. Pest's `arch()` helper makes boundaries executable:

```php
// tests/Arch/BoundaryTest.php
arch('Ordering does not import Inventory classes directly')
    ->expect('Ordering')
    ->not->toUse('Inventory\\Infrastructure');

arch('Domain layer stays pure')
    ->expect('Ordering\\Domain')
    ->not->toUse(['Illuminate\\Database', 'Illuminate\\Http']);

arch('Application layer does not touch HTTP')
    ->expect('Ordering\\Application')
    ->not->toUse('Illuminate\\Http\\Request');

```

These run in milliseconds and fail the CI pipeline the moment someone takes a shortcut.

Shared Kernel: What Belongs There
---------------------------------

Some primitives are genuinely cross-cutting: `Money`, `UserId`, base `DomainEvent`, audit interfaces. Keep a tiny `SharedKernel` module that all others may import. Keep it ruthlessly small — if it grows, you have a hidden god-module.

Migration Strategy for Existing Apps
------------------------------------

1. Identify the noisiest coupling hotspot (usually `User` or `Order`).
2. Extract its domain logic into a module directory without moving files yet.
3. Add the arch test — it will fail. Fix violations one PR at a time.
4. Move files once tests are green.

Small, incremental, always shippable.

Takeaways
---------

- Modules communicate through contracts (interfaces) or domain events — never direct class imports across boundaries.
- Each module owns its service provider, routes, and migrations.
- Pest `arch()` tests are the cheapest way to keep boundaries honest in CI.
- A shared kernel is fine; keep it tiny and dependency-free.
- Extract incrementally: enforce the rule first, then move the files.

- [laravel](https://msaied.com/articles?search=laravel)
- [architecture](https://msaied.com/articles?search=architecture)
- [ddd](https://msaied.com/articles?search=ddd)
- [modular-monolith](https://msaied.com/articles?search=modular-monolith)
- [pest](https://msaied.com/articles?search=pest)

 Frequently asked questions 
---------------------------

  How is a modular monolith different from just organising files into folders?Folder organisation is cosmetic. A modular monolith enforces that modules communicate only through declared contracts or events, never via direct class imports across boundaries. Arch tests in CI make that rule executable rather than advisory.

   Should each module have its own database schema or tables?Not necessarily. You can share a single database while keeping migrations per module. If two modules need the same data, expose it through a contract rather than sharing an Eloquent model — that keeps the persistence detail internal to each module.

   When should I split a module into a real microservice?When the module has a genuinely different scaling profile, a separate deployment cadence, or a team that needs full autonomy. Because your contracts are already interfaces, extracting to a service means swapping the binding for an HTTP or gRPC client — the calling code changes nothing.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://msaied.com#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Horizon Deep Dive: Queue Metrics, Supervisor Tuning, and Graceful Scaling](https://msaied.com/articles/laravel-horizon-deep-dive-queue-metrics-supervisor-tuning-and-graceful-scaling)  

   On this page
-------------

1. [Why a Modular Monolith?](#why-a-modular-monolith)
2. [Directory Layout](#directory-layout)
3. [Defining Internal Contracts](#defining-internal-contracts)
4. [Communicating Across Contexts With Domain Events](#communicating-across-contexts-with-domain-events)
5. [Enforcing Boundaries With Pest Arch Tests](#enforcing-boundaries-with-pest-arch-tests)
6. [Shared Kernel: What Belongs There](#shared-kernel-what-belongs-there)
7. [Migration Strategy for Existing Apps](#migration-strategy-for-existing-apps)
8. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/798/6a3e8f3b911d933877fa7335fb546431.png)  · 3 min read### Laravel Horizon Deep Dive: Queue Metrics, Supervisor Tuning, and Graceful Scaling

20 Oct 2026 ](https://msaied.com/articles/laravel-horizon-deep-dive-queue-metrics-supervisor-tuning-and-graceful-scaling) [ ![](https://cdn.msaied.com/796/13df3306c87eeeccf69b380e7a07d8da.png)  · 3 min read### Eloquent Custom Casts: Encrypting, Compressing, and Composing Value Objects at the Model Layer

19 Oct 2026 ](https://msaied.com/articles/eloquent-custom-casts-encrypting-compressing-and-composing-value-objects-at-the-model-layer) [ ![](https://cdn.msaied.com/797/2903a8f469c2bc085780ed09b63d2dd9.png) Laravel · 4 min read### Run Large PlanetScale Queries in Laravel With DB::olap()

19 Oct 2026 ](https://msaied.com/articles/run-large-planetscale-queries-in-laravel-with-dbolap) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/sitemap.xml)
