Layered Architecture: Presentation, Application, Domain, Infrastructure
Understand the four classic layers of a layered PHP application, what each layer is responsible for, and why dependencies must point in one direction only — outer layers depending on inner ones, never the reverse.
Layered architecture organizes an application into horizontal slices — Presentation, Application, Domain, and Infrastructure — stacked so that each layer only talks to the layer directly beneath it. Where MVC describes how a single request is handled, layered architecture describes how an entire codebase is organized so that business logic does not become tangled with HTTP handling on one side or database code on the other. The rule that makes it work, and the rule most PHP projects break within a few months, is the direction of dependency: outer layers may depend on inner layers, but inner layers must never know that outer layers exist.
The four layers and their responsibilities
Presentation is the outermost layer: HTTP controllers, CLI commands, API endpoints — anything that talks to the outside world and translates it into calls the Application layer understands. Application is the orchestration layer: it coordinates a use case ("place an order") by calling into the Domain and Infrastructure, but it holds little business logic of its own — mostly sequencing and transaction boundaries.
Domain is the core: entities, value objects, and business rules that would exist even if there were no database or web framework — this is where 'an order cannot ship unpaid' actually lives. Infrastructure is everything technical and swappable: the database access code, the email sender, the payment gateway client, the filesystem — concrete implementations of things the Domain only describes abstractly.
• Presentation — HTTP/CLI entry points; translates external input into Application calls and formats the response • Application — orchestrates use cases; coordinates Domain and Infrastructure, manages transactions, has little logic of its own • Domain — entities, value objects, business rules; the core of the application, framework-agnostic • Infrastructure — databases, external APIs, filesystems, mail; concrete technical implementations
The dependency direction rule
The layers are usually drawn stacked, with Presentation on top and Infrastructure on the bottom, but the important rule is not the stacking — it is that dependencies only point inward/downward through interfaces the inner layer defines, never sideways in reverse. Presentation depends on Application. Application depends on Domain. When Application needs something technical (save this order, send this email), it depends on an interface that the Domain or Application layer defines, and Infrastructure provides the implementation.
This inversion is what keeps the Domain layer pure: the Domain defines a PaymentGateway interface it needs, and the Infrastructure layer supplies StripePaymentGateway implements PaymentGateway — the Domain never imports anything from Infrastructure, Infrastructure imports from and implements what the Domain declares. If you ever see a Domain class with a use statement pointing at a Doctrine, Eloquent, or Guzzle class, the dependency rule has been violated.
Why the direction matters in practice
When dependencies point the wrong way — say, a Domain entity that extends an ORM base class, or business logic that calls $_SESSION directly — the business rules become impossible to test without bootstrapping a database or a web server, and impossible to reuse if you ever need a second Presentation layer (a CLI import tool, a public API) alongside the web one.
Keeping the arrows pointed one way means the Domain layer can be unit-tested with plain PHP objects and no I/O, the Infrastructure layer can be swapped (MySQL today, PostgreSQL tomorrow) without touching business rules, and multiple Presentation layers can share the same Application and Domain code.
Layer boundaries expressed with an interface
The interface lives in the Domain namespace — Domain declares the contract, not the implementation. Infrastructure imports from Domain to implement the interface; Domain never imports from Infrastructure. This is dependency inversion: the high-level Domain layer does not depend on the low-level Infrastructure layer, both depend on the interface.
// Domain layer — defines what it needs, owns no implementation detail
namespace App\Domain;
interface UserRepository
{
public function find(int $id): User;
public function save(User $user): void;
}
// Infrastructure layer — implements the Domain's interface
namespace App\Infrastructure;
use App\Domain\{User, UserRepository};
final class MysqlUserRepository implements UserRepository
{
public function __construct(private PDO $pdo) {}
public function find(int $id): User { /* SQL here */ }
public function save(User $user): void { /* SQL here */ }
}Code Examples
Presentation calls Application (PayOrderUseCase), Application calls the Domain entity and an interface it depends on, and Infrastructure supplies the real implementation of that interface — dependencies only ever point from outer to inner. Expected result: Order #1 marked as paid.
final class PdoOrderRepository implements OrderRepository — Infrastructure depends on and implements the Domain's interface, never the other way around.
<?php
namespace App\Domain;
// Domain: pure business rule, no framework, no database
final class Order
{
private bool $paid = false;
public function __construct(public readonly int $id, public readonly float $totalCents) {}
public function pay(): void
{
if ($this->totalCents <= 0) {
throw new \DomainException('Cannot pay an order with a non-positive total.');
}
$this->paid = true;
}
public function isPaid(): bool { return $this->paid; }
}
// Domain defines the interface it needs; it does not implement it
interface OrderRepository
{
public function find(int $id): Order;
public function save(Order $order): void;
}
namespace App\Application;
use App\Domain\OrderRepository;
// Application: orchestrates the use case, holds no business rule of its own
final class PayOrderUseCase
{
public function __construct(private OrderRepository $orders) {}
public function execute(int $orderId): void
{
$order = $this->orders->find($orderId);
$order->pay(); // the rule itself lives in the Domain, not here
$this->orders->save($order);
}
}
namespace App\Infrastructure;
use App\Domain\{O
…Layer stack and dependency direction
Arrows only point from outer layers to inner layers. Infrastructure is technically 'below' Domain in the stack but depends upward on Domain's interfaces — it never gets depended on directly by Domain.
┌─────────────────────────┐
│ Presentation │ HTTP controllers, CLI
└────────────┬──────────────┘
│ depends on
▼
┌─────────────────────────┐
│ Application │ use cases / orchestration
└────────────┬──────────────┘
│ depends on
▼
┌─────────────────────────┐
│ Domain │ entities, rules, interfaces
└────────────▲──────────────┘
│ implements (dependency inversion)
┌────────────┴──────────────┐
│ Infrastructure │ DB, mail, external APIs
└─────────────────────────┘Important Rules
Presentation depends on Application; Application depends on Domain; nothing inner depends back on something outer.
The Domain layer defines interfaces for what it needs (repositories, gateways); Infrastructure implements them — this is dependency inversion.
The Application layer orchestrates; it should contain little to no business logic of its own — that belongs in the Domain.
A Domain class should never import a framework, ORM, or HTTP class — if it does, the dependency direction has been violated.
Where this shows up in PHP frameworks
Symfony's convention of Controller -> Service -> Entity/Repository and Laravel's Controller -> Action/Service -> Model are informal versions of this layering. Teams that add an explicit Domain layer (plain PHP classes with no Eloquent or Doctrine base class) typically do so specifically to keep business rules testable and portable across a web API, a CLI importer, and a queue worker that all need the same rules enforced identically.
namespace App\Domain; class Order { public function pay(PDO $pdo) { $pdo->exec("UPDATE orders SET paid=1 WHERE id={$this->id}"); } } namespace App\Domain; class Order { public function pay(): void { $this->paid = true; } } // persistence handled separately by an OrderRepository in Infrastructure
The wrong version makes the Domain entity depend directly on PDO (Infrastructure), inverting the required dependency direction and making the entity impossible to unit test without a real database connection.
In layered architecture, how should the Domain layer obtain a service like sending email or charging a card? A. By calling the Infrastructure class directly, e.g. new SmtpMailer() B. By depending on an interface it defines itself, which Infrastructure implements C. By calling a global function defined in the Presentation layer D. The Domain layer should not need such services at all, ever
B. By depending on an interface it defines itself, which Infrastructure implements — The Domain defines the interface it needs (dependency inversion); Infrastructure provides the concrete implementation, so the Domain never imports anything from Infrastructure directly.
Which statement about the Application layer is correct? A. It should contain most of the business rules of the system B. It replaces the need for a Domain layer entirely C. It orchestrates use cases by coordinating Domain and Infrastructure, with little business logic of its own D. It is responsible for rendering HTML responses
C. It orchestrates use cases by coordinating Domain and Infrastructure, with little business logic of its own — The Application layer sequences a use case (call the domain, call a repository, commit a transaction) but the actual business rules live in the Domain layer, and rendering belongs to Presentation.
A controller currently calls Stripe's SDK directly to charge a card, then updates the order's paid status with raw SQL, all inside the controller method. Split this into the four layers: what goes in Domain, Application, Infrastructure, and what stays in Presentation?
Domain: Order::pay() plus a PaymentGateway interface. Application: a PayOrderUseCase that calls the gateway then order.pay() then repository.save(). Infrastructure: StripePaymentGateway implements PaymentGateway, and a PdoOrderRepository. Presentation: the controller that reads the request and calls PayOrderUseCase::execute().
Ask which parts are 'the rule' (paying makes an order paid), which are 'the sequence of steps', which are 'a swappable technical detail', and which are 'translate the HTTP request'.