Laravel Modular Monolith: Bounded Contexts in Practice | 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 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 dependency rules — without splitting into microservices or reaching for a DDD framework.

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

ShareCopy linkCopied

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

  On this page +1. [Why a Modular Monolith?](#why-a-modular-monolith)
2. [Module Structure](#module-structure)
3. [Internal Contracts: The Boundary Enforcement Mechanism](#internal-contracts-the-boundary-enforcement-mechanism)
4. [Enforcing the Rules with Deptrac](#enforcing-the-rules-with-deptrac)
5. [Cross-Module Events Instead of Direct Calls](#cross-module-events-instead-of-direct-calls)
6. [Shared Kernel](#shared-kernel)
7. [Key Takeaways](#key-takeaways)

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

Microservices solve distribution problems you probably don't have yet. A modular monolith gives you the same conceptual separation — bounded contexts, explicit contracts, isolated domain logic — while keeping a single deployable, a single database transaction scope, and zero network overhead between modules.

The goal is not folder organisation for its own sake. It is **preventing accidental coupling** so that the Billing team can refactor invoicing without breaking the Shipping team's code.

---

Module Structure
----------------

Each bounded context lives under `app/Modules/{Context}/`. The internal layout mirrors a mini-application:

```php
app/Modules/
  Billing/
    Actions/
    Data/          # DTOs, value objects
    Domain/        # Eloquent models, domain services
    Http/          # Controllers, requests, resources
    Infrastructure/ # Repositories, external adapters
    Providers/
      BillingServiceProvider.php
    routes.php
  Shipping/
    ...

```

Each module registers itself via its own service provider, which is loaded from `bootstrap/providers.php` (Laravel 11+):

```php
// bootstrap/providers.php
return [
    App\Modules\Billing\Providers\BillingServiceProvider::class,
    App\Modules\Shipping\Providers\ShippingServiceProvider::class,
];

```

```php
// app/Modules/Billing/Providers/BillingServiceProvider.php
class BillingServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->loadRoutesFrom(__DIR__ . '/../routes.php');
        $this->loadMigrationsFrom(__DIR__ . '/../Infrastructure/Migrations');
    }

    public function register(): void
    {
        $this->app->bind(
            \App\Modules\Billing\Domain\Contracts\InvoiceRepository::class,
            \App\Modules\Billing\Infrastructure\EloquentInvoiceRepository::class,
        );
    }
}

```

---

Internal Contracts: The Boundary Enforcement Mechanism
------------------------------------------------------

Modules must never import each other's internal classes directly. Instead, each module exposes a **public API interface** in a `Contracts` namespace:

```php
// app/Modules/Billing/Contracts/BillingModuleInterface.php
namespace App\Modules\Billing\Contracts;

use App\Modules\Billing\Data\InvoiceData;

interface BillingModuleInterface
{
    public function issueInvoice(int $orderId, int $customerId): InvoiceData;
    public function getOutstandingBalance(int $customerId): Money;
}

```

Shipping resolves this interface through the container — it never touches `Billing\Domain\` directly:

```php
// app/Modules/Shipping/Actions/DispatchOrder.php
class DispatchOrder
{
    public function __construct(
        private readonly BillingModuleInterface $billing,
    ) {}

    public function handle(Order $order): void
    {
        $balance = $this->billing->getOutstandingBalance($order->customer_id);

        if ($balance->isNegative()) {
            throw new OutstandingBalanceException();
        }

        // dispatch logic ...
    }
}

```

---

Enforcing the Rules with Deptrac
--------------------------------

Folder conventions are worthless without tooling. [Deptrac](https://github.com/qossmic/deptrac) analyses static imports and fails CI when a module reaches across a boundary:

```yaml
# deptrac.yaml
deptrac:
  paths:
    - app/Modules
  layers:
    - name: Billing
      collectors:
        - type: directory
          value: app/Modules/Billing/
    - name: Shipping
      collectors:
        - type: directory
          value: app/Modules/Shipping/
  ruleset:
    Shipping:
      - Billing  # Shipping may only use Billing's public Contracts
    Billing: ~

```

Run `./vendor/bin/deptrac analyse` in CI. Any direct import of `Billing\Domain\` from `Shipping\` becomes a build failure.

---

Cross-Module Events Instead of Direct Calls
-------------------------------------------

For truly decoupled side-effects, prefer domain events over direct method calls:

```php
// Billing fires:
event(new InvoicePaid($invoiceId, $customerId, $amount));

// Shipping listens in its own EventServiceProvider:
Event::listen(InvoicePaid::class, ReleaseHeldShipments::class);

```

The event class lives in a shared `app/Events/` namespace (or a dedicated `app/Shared/` module) so neither context owns it.

---

Shared Kernel
-------------

Some primitives — `Money`, `Address`, `UserId` — are used everywhere. Place them in `app/Shared/` and treat that namespace as a read-only dependency for all modules. No module writes back to Shared.

---

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

- **One service provider per module** keeps registration isolated and explicit.
- **Public contract interfaces** are the only surface other modules may depend on.
- **Deptrac in CI** turns architectural rules into hard failures, not guidelines.
- **Domain events** decouple side-effects without requiring a message broker.
- **Shared kernel** is small and stable; resist the urge to dump utilities there.

- [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 separate databases per module?No. A modular monolith shares one database. The boundary is in code, not infrastructure. You can extract a module to a microservice later if needed, but separate databases are not a prerequisite.

   How do I handle shared Eloquent models that multiple modules need?Place truly shared models (e.g. User) in a Shared or Core module. Each consuming module accesses them through that shared namespace, never by reaching into another module's Domain folder.

   Is Deptrac the only option for enforcing boundaries?PHPArkitect is a PHP-native alternative with a fluent API. Both integrate well with GitHub Actions or any CI pipeline and serve the same purpose: turning architectural conventions into automated checks.

   ![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 articleDomain-Driven Design in Laravel: Value Objects, DTOs, and Actions Without Bloat](https://www.msaied.com/public/articles/domain-driven-design-in-laravel-value-objects-dtos-and-actions-without-bloat) [Next articleBuilding a Laravel Package: Service Providers, Auto-Discovery, and Config Merging](https://www.msaied.com/public/articles/building-a-laravel-package-service-providers-auto-discovery-and-config-merging-1)  

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

1. [Why a Modular Monolith?](#why-a-modular-monolith)
2. [Module Structure](#module-structure)
3. [Internal Contracts: The Boundary Enforcement Mechanism](#internal-contracts-the-boundary-enforcement-mechanism)
4. [Enforcing the Rules with Deptrac](#enforcing-the-rules-with-deptrac)
5. [Cross-Module Events Instead of Direct Calls](#cross-module-events-instead-of-direct-calls)
6. [Shared Kernel](#shared-kernel)
7. [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)
