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

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

Menu
----

Close 

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

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

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

 Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Framework
============================================================================

 Learn how to carve a Laravel application into bounded contexts using directory conventions, service providers, and Composer path repositories — without reaching for a dedicated package.

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

ShareCopy linkCopied

 ![Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Framework](https://cdn.msaied.com/190/6fc8d99e2eb72257ed8ce5a9e5176ae2.png) 

  On this page +1. [Why a Modular Monolith?](#why-a-modular-monolith)
2. [Directory Convention First](#directory-convention-first)
3. [Module Service Providers](#module-service-providers)
4. [Enforcing Boundaries with Deptrac](#enforcing-boundaries-with-deptrac)
5. [Cross-Module Communication](#cross-module-communication)
6. [Shared Kernel](#shared-kernel)
7. [Testing in Isolation](#testing-in-isolation)
8. [Key Takeaways](#key-takeaways)

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

Microservices solve distribution problems you probably don't have yet. A well-structured monolith that respects domain boundaries gives you most of the organisational benefits — independent deployability aside — without the operational overhead. The goal is to make cross-module coupling *visible and painful*, so it stays rare.

Directory Convention First
--------------------------

Start with a `modules/` directory at the project root. Each module is a self-contained PHP namespace:

```
modules/
  Billing/
    src/
      BillingServiceProvider.php
      Domain/
        Invoice.php
        InvoiceRepository.php
      Application/
        CreateInvoiceAction.php
      Infrastructure/
        EloquentInvoiceRepository.php
    composer.json
  Catalog/
    src/
      CatalogServiceProvider.php
      ...
    composer.json

```

Each module has its own `composer.json` declaring its namespace. The root `composer.json` pulls them in as path repositories:

```json
{
  "repositories": [
    {"type": "path", "url": "modules/Billing"},
    {"type": "path", "url": "modules/Catalog"}
  ],
  "require": {
    "acme/billing": "@dev",
    "acme/catalog": "@dev"
  }
}

```

Composer symlinks each module into `vendor/`, so autoloading works identically to a real package. The boundary is now enforced by Composer's dependency graph, not just a gentleman's agreement.

Module Service Providers
------------------------

Each module registers its own bindings, routes, and migrations:

```php
// modules/Billing/src/BillingServiceProvider.php
namespace Acme\Billing;

use Illuminate\Support\ServiceProvider;
use Acme\Billing\Domain\InvoiceRepository;
use Acme\Billing\Infrastructure\EloquentInvoiceRepository;

class BillingServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(InvoiceRepository::class, EloquentInvoiceRepository::class);
    }

    public function boot(): void
    {
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        $this->loadRoutesFrom(__DIR__.'/../routes/api.php');
    }
}

```

Register it in `bootstrap/providers.php` (Laravel 11+) or `config/app.php`:

```php
return [
    Acme\Billing\BillingServiceProvider::class,
    Acme\Catalog\CatalogServiceProvider::class,
];

```

Enforcing Boundaries with Deptrac
---------------------------------

Conventions break under deadline pressure. [Deptrac](https://github.com/qossmic/deptrac) statically analyses `use` statements and fails CI when a module imports from a sibling it shouldn't:

```yaml
# deptrac.yaml
layers:
  - name: Billing
    collectors:
      - type: namespace
        value: Acme\\Billing
  - name: Catalog
    collectors:
      - type: namespace
        value: Acme\\Catalog
ruleset:
  Billing:
    - Catalog   # Billing MAY depend on Catalog's public API
  Catalog: []   # Catalog must not depend on anything else

```

Run `deptrac analyse` in your pipeline. A `use Acme\Billing\...` inside `Catalog` now breaks the build.

Cross-Module Communication
--------------------------

Modules talk through three mechanisms only:

1. **Public contracts** — interfaces and DTOs in a `Contracts/` namespace that other modules may import.
2. **Laravel events** — fire a domain event; other modules listen without coupling to the source.
3. **The service container** — resolve a contract, never a concrete class from another module.

```php
// Catalog fires an event; Billing listens
event(new ProductPurchased($productId, $userId));

// In BillingServiceProvider::boot()
Event::listen(ProductPurchased::class, GenerateInvoiceListener::class);

```

### Shared Kernel

Put truly cross-cutting concerns — `Money`, `UserId`, base exceptions — in a `SharedKernel` module that every other module may depend on. Keep it tiny.

Testing in Isolation
--------------------

Because each module is a Composer package, you can run Pest inside the module directory with its own `phpunit.xml`:

```bash
cd modules/Billing && vendor/bin/pest

```

Mock the `InvoiceRepository` interface; never touch the database in unit tests. Integration tests live at the root and boot the full application.

Key Takeaways
-------------

- Use Composer path repositories to make module boundaries real, not aspirational.
- Each module owns its service provider, migrations, and routes.
- Deptrac in CI prevents accidental cross-module coupling before it becomes technical debt.
- Cross-module communication flows through events, contracts, and the container — never direct class imports.
- A `SharedKernel` module holds value objects and base types; keep it deliberately small.

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

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

  Do I need a dedicated package like nwidart/laravel-modules?No. Composer path repositories plus a per-module ServiceProvider give you the same autoloading and isolation without an additional dependency. Third-party module packages add conventions you may not agree with; rolling your own keeps full control.

   How do I handle shared database tables across modules?Prefer each module owning its own tables. When two modules genuinely share data, expose it through a repository contract in the SharedKernel or the owning module's public API. Direct cross-module Eloquent model imports are a coupling smell.

   Can this structure work with Filament admin panels?Yes. Each module can register its own Filament resources inside its ServiceProvider using Filament::serving() or a dedicated panel provider. This keeps admin UI co-located with the domain it manages.

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

[Mohamed Said](https://www.msaied.com/public#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://www.msaied.com/public#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 articleSubscriptionify: Feature-Based Subscription Management for Laravel](https://www.msaied.com/public/articles/subscriptionify-feature-based-subscription-management-for-laravel) [Next articleDomain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat](https://www.msaied.com/public/articles/domain-driven-design-in-laravel-actions-dtos-and-value-objects-without-bloat)  

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

1. [Why a Modular Monolith?](#why-a-modular-monolith)
2. [Directory Convention First](#directory-convention-first)
3. [Module Service Providers](#module-service-providers)
4. [Enforcing Boundaries with Deptrac](#enforcing-boundaries-with-deptrac)
5. [Cross-Module Communication](#cross-module-communication)
6. [Shared Kernel](#shared-kernel)
7. [Testing in Isolation](#testing-in-isolation)
8. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

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

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

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

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

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

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

 [Discuss your project ↗](https://www.msaied.com/public#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://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/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://www.msaied.com/public/sitemap.xml)
