Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Enforcing Layered Architecture Dependency Direction

Welcome back. In the previous lesson, WorkshopBookings protected the rule that confirmed places cannot exceed the approved booking limit. Its confirm() method could make that decision without knowing anything about Laravel. The next challenge is to preserve that independence when an HTTP request needs to load the bookings, confirm a place, and save the result.

This lesson focuses on dependency direction: which parts of the code may name and rely on which other parts. You’ll use presentation, application, domain, and infrastructure layers to keep the workshop rule independent of both HTTP and Eloquent. Allow about 40 minutes for the video, reading, and worked PHP example.


Four responsibilities, one governing rule

Watch the opening of Clean Architecture vs Domain-Driven Design (DDD) – Understand the Difference by Amichai Mantinband. Its banking example distinguishes the layer that coordinates a use case from the layer that makes a business decision.

Clean Architecture vs Domain-Driven Design (DDD) - Understand the Difference

Watch this explanation to establish what belongs in each layer before examining dependency direction in code.

Watch the layer overview for presentation, application, domain, and infrastructure responsibilities. Then watch the banking example, paying particular attention to the distinction between coordinating a workflow and deciding whether a business rule permits it.

For Reservations, the responsibilities are:

Layer Responsibility in a confirmation request
Presentation Accept an HTTP request, validate its transport-level shape, invoke a use case, and produce an HTTP response.
Application Coordinate confirm a reservation: obtain the relevant bookings, ask the domain to confirm, and arrange to save them.
Domain Decide whether confirmation is allowed while protecting the booking-limit invariant.
Infrastructure Implement storage with Eloquent and the database.

These are logical boundaries, not four servers and not necessarily four Composer packages. The distinction matters because code can be in separate folders yet remain tightly coupled.

The governing rule is that source-code dependencies point toward the core. The domain must not import application, presentation, or infrastructure code. Application code may use the domain, but must not require a controller or an Eloquent model. Presentation and infrastructure may depend on contracts defined by the application. A Laravel service provider can connect those outer pieces when the program starts.

The concentric circles place entities and use cases inside interface adapters and frameworks. The accompanying port diagram helps distinguish the code’s dependency on an inner interface from a request’s runtime passage through an outer adapter.

The picture is useful, but do not interpret its circles as a mandate for a particular folder layout. The test is more concrete: if you replaced Laravel’s HTTP entry point or Eloquent persistence, which files would you have to edit? A change to either should not force an edit to WorkshopBookings.confirm().


A call to the database need not be a dependency on Eloquent

This is the part that can initially seem contradictory. The application use case must load and save bookings. Surely it depends on the database?

It depends on the capability to load and save bookings, but need not depend on the implementation of that capability. Read the relevant parts of Milan Jovanović’s Dependency Rule in Clean Architecture Explained. Its examples use .NET; focus on the relationships between types rather than the language-specific syntax.

The Dependency Rule in Clean Architecture - Complete Guide

Read Milan Jovanović’s explanation to separate source-code dependencies from runtime calls, and to see why the application owns the interface it needs.

In “What Is the Dependency Rule?”, begin with “Strip away the diagrams and the folder structures” and read the core rule through the inward-dependency description. In “Dependency Inversion,” begin “The Application layer defines interfaces” and read the interface example through the explanation of abstractions. Finally, in “Flow of Control vs Source Dependencies,” begin “Here's the part that confuses most people” and read the request walkthrough through its distinction between runtime calls and source references. Ask what type the use case names in its source code, even when the eventual call executes database code.

Apply that distinction to Reservations. The application layer can define the operation it requires:

<?php

namespace App\Reservations\Application;

use App\Reservations\Domain\WorkshopBookings;

interface WorkshopBookingsRepository
{
    public function get(string $workshopId): WorkshopBookings;

    public function save(WorkshopBookings $bookings): void;
}

The interface says nothing about tables, Eloquent relationships, or SQL. Its language is shaped by the use case: obtain the bookings for a workshop and save their changed state. This is an application-owned port. An infrastructure class will implement it.

Now the use case can coordinate the operation without making the admission decision itself:

<?php

namespace App\Reservations\Application;

final class ConfirmReservation
{
    public function __construct(
        private WorkshopBookingsRepository $bookings,
    ) {}

    public function __invoke(
        string $workshopId,
        string $reservationId,
    ): void {
        $workshopBookings = $this->bookings->get($workshopId);

        $workshopBookings->confirm($reservationId);

        $this->bookings->save($workshopBookings);
    }
}

The important line is confirm(). The application service decides when to ask for confirmation; the domain entity decides whether confirmation is legal. If the limit is already reached, the domain rejects the operation regardless of whether the caller was an API controller, a command-line tool, or another adapter.

The use case names WorkshopBookingsRepository, an application interface. It does not name an Eloquent model, Request, or a database facade. At runtime its repository calls will execute infrastructure code, but the source dependency of ConfirmReservation remains within the application layer.


Keep framework work at the edges

An HTTP controller can translate a request into the use case’s plain inputs:

<?php

namespace App\Http\Controllers\Reservations;

use App\Reservations\Application\ConfirmReservation;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ConfirmReservationController
{
    public function __invoke(
        Request $request,
        ConfirmReservation $confirm,
    ): JsonResponse {
        $data = $request->validate([
            'workshop_id' => ['required', 'string'],
            'reservation_id' => ['required', 'string'],
        ]);

        $confirm($data['workshop_id'], $data['reservation_id']);

        return response()->json(['status' => 'confirmed']);
    }
}

Request and JsonResponse belong here: this class speaks HTTP. The validation shown establishes that the required inputs have the expected shape; it does not replace the domain’s booking-limit rule. Notice also that no Eloquent model passes from the controller into the use case.

On the other side, an EloquentWorkshopBookingsRepository belongs in infrastructure. It can query Eloquent records and map them to and from WorkshopBookings, while implementing the application-owned interface. The mapper would need to reconstruct the root’s authoritative confirmed-reservation state—not merely its workshop ID and limit—or confirm() would decide using incomplete facts.

At startup, Laravel needs to know which implementation fulfills the port. A service provider can register the binding:

<?php

namespace App\Providers;

use App\Reservations\Application\WorkshopBookingsRepository;
use App\Reservations\Infrastructure\EloquentWorkshopBookingsRepository;
use Illuminate\Support\ServiceProvider;

final class ReservationServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            WorkshopBookingsRepository::class,
            EloquentWorkshopBookingsRepository::class,
        );
    }
}

The provider is a composition root: wiring application contracts to concrete implementations is its job. Its knowledge of infrastructure is an intentional exception at the outer edge; it is not permission for controllers or use cases to import infrastructure classes. The Eloquent implementation depends on the application interface it implements, while ConfirmReservation remains unaware of the implementation Laravel selected.

This sketch establishes dependency direction, not a complete storage design. In particular, loading a root, calling confirm(), and saving it must eventually be protected against concurrent confirmations. A correct repository mapping and appropriate transaction or concurrency mechanism are still needed. Keeping the domain independent does not make a read–modify–write sequence automatically atomic.


Make the direction a codebase rule

Architecture becomes more dependable when a prohibited dependency can fail a build rather than relying only on review. For this feature, a useful boundary policy is:

  • Domain code must not import Laravel, controllers, application services, or Eloquent models.
  • Application code may import domain types, but not Laravel, controllers, Eloquent models, or infrastructure implementations.
  • Reservation controllers may import application types and Laravel HTTP types, but not the reservation infrastructure implementation.
  • Infrastructure may import the application port and domain types needed for mapping. Wiring belongs in the service provider.

In PHP, placing classes in directories does not enforce those rules by itself; an import across directories can still work. A first CI guard could scan forbidden imports and fail on a violation. For example, the following rule set expresses the critical boundaries for the paths used above:

$forbiddenImports = [
    'app/Reservations/Domain' => [
        'App\\Reservations\\Application\\',
        'App\\Reservations\\Infrastructure\\',
        'App\\Http\\',
        'App\\Models\\',
        'Illuminate\\',
    ],
    'app/Reservations/Application' => [
        'App\\Reservations\\Infrastructure\\',
        'App\\Http\\',
        'App\\Models\\',
        'Illuminate\\',
    ],
    'app/Http/Controllers/Reservations' => [
        'App\\Reservations\\Infrastructure\\',
        'App\\Models\\',
    ],
];

A checker can examine PHP imports under each path against those prefixes and run in CI. That simple check is a starting guard, not proof of complete isolation: fully qualified class references inside method bodies, dynamic container lookups, and framework calls through other mechanisms need coverage too. For a durable rule, use a namespace-aware architecture check and supplement it with review of boundary-crossing code. The important enforcement property is that an application change importing EloquentWorkshopBookingsRepository cannot silently pass as routine code.

Not every reference needs an interface. ConfirmReservation can depend directly on WorkshopBookings: that is an inward dependency on the domain, not a violation. The port exists where the use case needs an outer capability without inheriting its implementation details. Adding interfaces between every pair of classes would obscure rather than strengthen the boundary.

In a React and Laravel application, React communicates with Laravel over HTTP; it does not import these PHP classes. Within Laravel, the controller is the presentation adapter for that boundary. This distinction will matter next when you trace a complete feature from a React interaction through the API and into persistent storage.


Bringing the boundary together

The dependency rule protects the code that expresses your most durable decisions. WorkshopBookings owns admission, ConfirmReservation coordinates its use, the controller handles HTTP, and an infrastructure implementation handles persistence. The application’s repository interface lets execution reach the database without requiring the use case’s source code to name Eloquent.

The practical senior-engineering move is to combine that design with enforcement: define permitted references, keep composition in one outer place, and make forbidden dependencies visible in CI. In the next lesson, you’ll trace the reservation feature across the React and Laravel boundary all the way to storage, distinguishing each component’s responsibility along the way.

Can't find a good explanation? Sign up and we'll make it for you

Sign up