Hexagonal Architecture

A universal travel adapter doesn't rewire your laptop charger for every country it visits — the charger's own logic (voltage regulation, charge rate) stays exactly the same; only the small adapter piece changes to fit whichever wall socket is on the other end that day. Hexagonal architecture applies the same idea to code: the core business rules stay agnostic of what they're plugged into, and a thin port/adapter layer absorbs whichever database, queue, or third-party API happens to be on the other side.

I don't reach for a full ports-and-adapters layer by default — most of the Laravel apps and Magento modules I've built are small enough that a repository interface or two already gives me the swappable boundary I need, without a dedicated hexagon of layers wrapped around it. The criterion I actually use: once a project has more than one delivery mechanism talking to the same core logic — a web frontend and a queued job, an admin panel and a mobile app, or, as in Pharmalytics (the pharma-lab ERP I'm building on my own time), a scenario where a second data path or reporting mechanism becomes genuinely plausible — that's when keeping the domain rules behind an explicit port, with adapters plugged in on either side, starts paying for itself. Below that bar, the extra interface and adapter classes are just ceremony.

Trade-off: every port and adapter is one more file to open before reaching the code that actually talks to the database or queue — for a solo project or a small team, that's real friction a new contributor has to push through before they can trace a request end to end.

OrderController
SqliteDatabase

Direct coupling

class OrderController
{
    public function store(Request $request): void
    {
        $db = new SqliteDatabase(config('database.path'));

        $db->insert('orders', $request->validated());
    }
}

Ports & adapters

interface InventoryRepository
{
    public function save(array $order): void;
}

class SqliteAdapter implements InventoryRepository
{
    public function save(array $order): void
    {
        $this->connection->insert('orders', $order);
    }
}
// OrderController now depends on InventoryRepository, not SqliteDatabase directly
{CC}