Back to Blog
Modular Monolith: The Best of Both Worlds
Why Modular Monolith?
A modular monolith gives you the development simplicity of a monolith with the organizational benefits of microservices. Modules have clear boundaries and can be extracted to services later if needed.
Module Structure
app/
├── Modules/
│ ├── Catalog/
│ │ ├── Controllers/
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Events/
│ │ ├── Listeners/
│ │ ├── CatalogServiceProvider.php
│ │ └── routes.php
│ ├── Orders/
│ ├── Inventory/
│ └── Shipping/
├── Shared/
│ ├── Contracts/
│ ├── Events/
│ └── ValueObjects/
Module Boundaries
// Modules communicate through interfaces
namespace App\Modules\Orders\Contracts;
interface InventoryChecker
{
public function isAvailable(string $productId, int $quantity): bool;
}
// Implementation in Inventory module
namespace App\Modules\Inventory\Services;
class InventoryService implements InventoryChecker
{
public function isAvailable(string $productId, int $quantity): bool
{
return Stock::where('product_id', $productId)
->where('quantity', '>=', $quantity)
->exists();
}
}
Module Registration
// CatalogServiceProvider.php
class CatalogServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(ProductRepository::class, EloquentProductRepository::class);
}
public function boot(): void
{
$this->loadRoutesFrom(__DIR__ . '/routes.php');
$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
}
}
Cross-Module Communication
// Events for loose coupling
namespace App\Modules\Orders\Events;
class OrderPlaced
{
public function __construct(
public readonly string $orderId,
public readonly array $items
) {}
}
// Inventory listens and reacts
class ReserveInventory
{
public function handle(OrderPlaced $event): void
{
foreach ($event->items as $item) {
$this->inventory->reserve($item['product_id'], $item['quantity']);
}
}
}
Testing Modules
class OrderModuleTest extends TestCase
{
public function test_places_order_with_available_inventory(): void
{
// Mock the inventory contract
$this->mock(InventoryChecker::class)
->shouldReceive('isAvailable')
->andReturn(true);
$response = $this->postJson('/api/orders', $this->validOrderData);
$response->assertCreated();
}
}
Conclusion
A modular monolith provides clear boundaries without distributed complexity. Start here, and extract services only when you have proven need.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch