2025-12-07
#architecture
#laravel
#backend

Modular Monolith & Hexagonal Architecture

Vertical slicing, Hexagonal Architecture, and Pragmatic Isolation in Laravel.

The project is structured as a Modular Monolith. This means code is divided into independent vertical slices (Domains) that can interact only through defined contracts.

Module Structure

Each domain (app/Domains/Auth, app/Domains/Reservation) is self-contained and includes:

  • Entities, Services, DTOs (Domain Layer)
  • UseCases (Application Layer)
  • Controllers, Repositories (Infrastructure Layer)
  • Providers (Dependency Injection)

Hexagonal Architecture (Ports & Adapters)

For Cross-Module Communication, I use Hexagonal Architecture.

Rule:

Module A (Consumer) never depends on specific classes of Module B (Provider). It depends only on the Interface (Port) that it defines itself. This ensures:

  • Low Coupling: Changes in the User module implementation do not break the Auth module.
  • Inversion of Control: Dependencies point towards abstractions, not details.

Interaction Scheme (Auth -> User)

text
Domain: Auth (Consumer)                Domain: User (Provider)
┌─────────────────────────┐           ┌──────────────────────────────────┐
│                         │           │                                  │
│  [AuthUseCase]          │           │  [AuthUserProviderAdapter]       │
│       │ (Calls)         │           │        (Adapter)                 │
│       ▼                 │           │            │                     │
│  [UserProviderInterface]│<──────────│────────────┘ (Implements)        │
│       (Port)            │           │            │                     │
│                         │           │            ▼                     │
│                         │           │   [UserRepository]               │
└─────────────────────────┘           │   (Internal Logic)               │
                                      │                                  │
                                      └──────────────────────────────────┘
  1. Port: The Auth domain defines the UserProviderInterface. "I need to find a user by email".
  2. Adapter: The User domain implements this interface in AuthUserProviderAdapter.
  3. Binding: The ServiceProvider binds the interface to the implementation.

Advantages

  1. Isolation: You can rewrite the User module without breaking Auth.
  2. Testability: The Auth module is easy to test by mocking UserProviderInterface.
  3. Scalability: If you need to extract User into a microservice, simply rewrite the Adapter to use an HTTP client.

Pragmatic Data Isolation

I follow the Pragmatic Modular Monolith approach:

  1. Write Operations (Command): Strictly via API.

    • It is forbidden to modify another module's data directly (e.g., UPDATE/INSERT on the users table from the Reservation module).
    • This guarantees that the Data Owner's business rules are always executed.
  2. Read Operations (Query):

    • Using JOINs with other modules' tables is allowed for building efficient queries (lists, reports) to avoid N+1 issues.
    • However, remember: strong coupling at the DB level will complicate potential future separation into microservices.

Connected Thoughts

Egor Zdioruc | Lead Full Stack Developer | Laravel & AI Solutions