Skip to content

Hexagonal Architecture and CQRS

Fight Common keeps business language at the center and makes frameworks replaceable at the edge. Source-code dependencies point inward: Domain code knows no framework; Application code coordinates the Domain through ports; Adapter code connects those ports to databases, transports, and frameworks.

The dependency rule

src/Adapter Adapter

Framework composition, persistence, queues, HTTP clients, files, and provider integrations.

Replaceable edge
src/Application Application

Use-case coordination, ports, buses, handlers, filters, subscribers, and transaction boundaries.

Stable boundary
src/Domain Domain

Business values, rules, specifications, messages, collections, and repository vocabulary.

Protected center
Solid arrow means “source code may depend on.” No arrow points back toward a framework.

This direction is about ownership, not request flow. A web request may enter through Symfony, Laravel, Yii, CodeIgniter, Slim, or framework-free composition. The Adapter translates it into an Application operation; the Application coordinates Domain behavior; results travel back out without making the Domain depend on the caller.

src/Standards is deliberately outside this chain. It publishes development-time coding policy and has no runtime dependents. Domain, Application, and Adapter code cannot use it, and it cannot use runtime code.

Ports and adapters

A port is a contract owned by the code that needs the capability. An adapter fulfills that contract using a particular technology. Framework bootstrapping chooses the adapter; portable Domain and Application code does not.

Application owns the need MailTransport

MailService sends an application message through this port without knowing which mailer exists.

Adapter owns the technology SymfonyMailTransport

The adapter translates the portable message into Symfony Mailer calls and infrastructure failures.

Repositories use the same separation. Domain-owned Pagination and ResultSet express business-facing query vocabulary. Application-owned TransactionalUnitOfWork marks the atomic use-case boundary. The DoctrineTransactionalUnitOfWork adapter supplies the ORM-specific implementation. Your application owns its aggregate repository port because only your application can define what loading and saving that aggregate means.

Framework packages belong at the composition edge. Their service providers, compiler passes, and configuration bind Fight Common contracts to selected adapters. They may call inward; they do not move framework types into Domain or portable Application code.

CQRS message flows

CQRS separates requests to change state from requests to read it. Events then announce facts that have already happened. Fight Common keeps the message payloads in Domain, the coordination contracts in Application, and the routing or transport mechanisms in Adapter.

Commandrequests one mutation
Domain messageCommand
Application contractCommandBus::execute()filters wrap dispatch
One handlerCommandHandler::handle()coordinates mutation
Queryasks for a result
Domain messageQuery
Application contractQueryBus::fetch()filters wrap dispatch
One handlerQueryHandler::handle()returns read data
Eventannounces a fact to zero or more listeners
Domain messageEvent
Application contractEventDispatcher::trigger()registration maps event types
SubscribersEventSubscriberindependent reactions
Solid arrow follows dispatch. Dashed arrow marks the query result returning to its caller. The doubled event arrow marks fan-out.

What each layer owns

  • Domain owns Command, Query, and Event payload contracts plus CommandMessage, QueryMessage, and EventMessage envelopes. Messages carry identity, metadata, timestamp, and the typed payload without knowing how they will be delivered.
  • Application owns CommandBus, QueryBus, EventDispatcher, handlers, filters, and subscribers. Handlers coordinate a use case; filters wrap command or query dispatch for concerns such as validation or metrics; subscribers declare which event facts they react to.
  • Adapter owns routing, pipelines, service-container lookup, and transport. Routing command and query buses match one handler. Simple and service-aware event dispatchers invoke registered listeners. Messenger, Laravel, and CodeIgniter adapters can move supported commands and events through asynchronous infrastructure.

Queries are synchronous because fetch() returns a result. Commands and events can use synchronous or asynchronous adapters, selected at composition time. Asynchronous delivery changes transport and failure timing; it does not change which layer owns the message or its handler contract.

The Quick Start shows these parts in one order-processing path: a command handler uses repository and payment ports, commits through TransactionalUnitOfWork, and triggers an event that a subscriber turns into a follow-up command.

Repository enforcement

The architecture exists in the public contracts and dependency direction first. Deptrac is repository evidence that the implementation still matches that model.

./bin/deptrac

The runtime configuration classifies every production class, rejects outward dependencies, and gives each layer an explicit external-package allowance. Domain remains framework-free. Application may use PHP internals, neutral PSR contracts, and the allowlisted scheduler expression contract. Adapter integrations and their third-party packages are named explicitly. A separate unassigned-token check fails if production or Standards code escapes classification. There is no baseline of accepted violations.

Deptrac is a Fight Common development dependency, not a runtime requirement for consumers. A consuming project can install it independently and define boundaries for its own namespaces; copying Fight Common’s package-specific configuration would not describe that project’s architecture.

Next steps