Europe/Paris
Posts

Understanding hexagonal architecture with Symfony

June 28, 2026 · 2 min de lecture
In a typical Symfony project, it's easy to let business logic leak into controllers, Doctrine entities (Doctrine is Symfony's default ORM — the library that maps database tables to PHP objects), or services coupled directly to the framework. The result: code that's hard to unit test, and a strong dependency on infrastructure (database, SOAP — an older, XML-based protocol for calling remote services, still common in enterprise systems — third-party APIs). Hexagonal architecture — also known as ports & adapters — proposes a strict separation between:
  • The business domain, independent of any framework
  • Ports, interfaces that define what the domain needs
  • Adapters, which implement those ports with a concrete technology (Doctrine, SOAP, REST, etc.)
src/
├── Domain/
│   ├── Model/
│   └── Port/
├── Application/
│   └── UseCase/
└── Infrastructure/
    ├── Persistence/
    └── Http/
The domain never knows about Symfony, Doctrine, or an HTTP library. It only defines interfaces (ports). Infrastructure implements those interfaces.
Php
// Domain/Port/BeneficiaireRepositoryInterface.php
interface BeneficiaireRepositoryInterface
{
    public function findByNir(string $nir): ?Beneficiaire;
}
Php
// Infrastructure/Persistence/DoctrineBeneficiaireRepository.php
final class DoctrineBeneficiaireRepository implements BeneficiaireRepositoryInterface
{
    public function findByNir(string $nir): ?Beneficiaire
    {
        // Doctrine logic here, invisible to the domain
    }
}
The business use case depends only on the interface, never on the Doctrine implementation. Result: testable in isolation with a simple mock (a fake stand-in object that simulates a dependency) or an in-memory repository (a fake implementation that stores data in a plain array instead of a real database), no database required.
  • Fast unit tests on business logic, with no full Symfony bootstrap (the framework's startup sequence — loading configuration, initializing services, etc. — that normally runs before every test)
  • Easier infrastructure changes: replacing a SOAP call with a REST API becomes a change localized to the adapter, without touching the domain
  • More readable separation of concerns for code reviews
This architecture adds indirection and more files for simple cases. It makes sense for a complex business core meant to live for years, much less so for a basic CRUD (Create, Read, Update, Delete — an app that just moves data in and out of a database). Judge it case by case rather than applying it everywhere on principle. Hexagonal architecture isn't an end in itself — it's a tool to isolate business complexity from technical details. On a backend project meant to live for several years and change infrastructure over time, the initial investment pays off quickly.
On this page
Book a call