Enforcing Module Boundaries in a Laravel Modular Monolith A developer published a PHPUnit-based test that enforces module boundaries in Laravel modular monoliths, using PHP's built-in tokenizer to extract top-level use imports and flag any module that imports another module's classes outside its public Contracts and Events namespaces. The approach requires PHP 8.0+ and PHPUnit 9, adds no dependencies beyond PHP itself, and was validated against a sample module tree where it caught a deliberate violation. The author argues the test is preferable to tools like Deptrac or PHPArkitect for a single dependency rule because it runs wherever existing tests run. Every modular monolith starts with good intentions and a folder per module. Six months later, Orders calls a private repository inside Billing because it was quicker, and the folders are just decoration. Module boundaries in a Laravel monolith are not enforced by the framework. If you want them to survive deadlines and new team members, you have to enforce module boundaries with something that fails a build, not with a wiki page. This article shows a dependency rule that fits most Laravel modular monoliths, a test that enforces it using nothing but PHP's tokenizer and PHPUnit, and how to adopt it in a codebase that already has violations. The test below is the exact code I ran against a sample module tree while writing this article, including the case where it caught a deliberate violation. If you are still deciding whether a modular monolith is right for you, start with building a modular monolith in Laravel that can grow https://parhamtehrani.com/en/blog/modular-monolith-laravel . Give every module the same three-part shape: app/Modules/ Billing/ Contracts/ public: interfaces other modules may type-hint Events/ public: facts other modules may listen to Internal/ private: models, repositories, services, jobs Orders/ Contracts/ Events/ Internal/ Then one rule: a module may import anything from itself, but from another module only its Contracts and Events . This captures the two legitimate ways modules interact. A synchronous question goes through an interface the owning module publishes and binds in its service provider. An asynchronous fact goes through an event the owning module dispatches. Everything else, including Eloquent models, is private. That last point is the one teams push back on, and the one that matters most: when Orders queries Billing 's tables through Billing 's models, the database schema becomes a shared API that nobody can change. php namespace App\Modules\Billing\Contracts; interface ChargesCustomers { public function charge int $customerId, int $amountInCents : string; } // Billing's service provider decides the implementation. $this- app- bind ChargesCustomers::class, StripeLikeGateway::class ; // Orders depends on the contract only. namespace App\Modules\Orders\Internal; use App\Modules\Billing\Contracts\ChargesCustomers; final class PlaceOrder { public function construct private ChargesCustomers $billing {} } Tools such as Deptrac or PHPArkitect can express rules like this and more. If your team already runs one, use it. But adding a new tool means new configuration, a new CI step, and a new thing to learn. A single PHPUnit test runs everywhere your tests already run, reads like any other test, and has no dependencies beyond PHP itself. For a rule this simple, that trade-off favours the test. The test reads every PHP file under app/Modules , extracts top-level use imports with token get all , and reports any import of another module's class outside the public namespaces. Using the tokenizer rather than a regular expression avoids false matches in comments and strings, and lets the test ignore trait imports inside class bodies, which use the same use keyword. Requires PHP 8.0+ it uses the T NAME QUALIFIED token and works with PHPUnit 9, which ships with Laravel 8 and 9 projects. php