Skip to content
Title ‘The missing layer.’ on a dark background with subtle vertical column lines

The missing layer: What happens between controller and business logic

Every complex PHP project starts with weeks of foundation work between framework and business logic. Why that gap exists, and why it does not have to.

Anyone who has set up a non-trivial PHP project knows the weeks between composer create-project and the first line of real business logic. Day one feels productive. Composer runs, the first routes answer, the first migrations land. Two weeks later the team still is not shipping features. It is building repositories, defining DTOs, debating the directory layout under src/Domain/, deciding for the fourth time which validation library to use, and assembling an aggregate skeleton by hand that will get torn apart again in the next sprint.

This phase is not a weakness of the team. It is a structural gap in the PHP stack, visible in almost every project the moment complexity moves beyond a pure CRUD application. The question is not why these weeks happen. The question is where in the stack they actually belong.

The Stack Picture That Almost Works

When someone explains a PHP project to an outsider, they usually draw a short chain:

HTTP Request
  → Web framework (Laravel, Symfony, Slim)
    → Controller
      → Business logic
        → Database

The picture is not wrong. It is incomplete. Between the controller and the business logic, a layer is missing that exists in every serious project but rarely gets named: the domain layer. Aggregates (in Domain-Driven Design: consistent clusters of business objects, like an order with its line items), repositories, command and query handlers, validation, domain events, bounded-context facades. Everything that keeps the controller from collapsing into a six-hundred-line monster, and keeps the business logic from being shot through with SQL.

This layer is not optional. The moment a project has more than one functional area, more than one team working in parallel, or business rules that touch more than a single row, the layer appears whether anyone planned for it. The only real question is whether it emerges as a deliberate layer or as sediment scattered across controllers and services.

Why Frameworks Do Not Provide This Layer

It would be easy to blame the frameworks. Symfony has Doctrine. Laravel has Eloquent. Both have service containers, validator components, event dispatchers. Why is that not enough?

Because frameworks solve a different problem. A web framework handles everything between the HTTP request and an executed piece of PHP code: routing, request parsing, authentication, dependency injection, response serialization, middleware chains, HTTP caching, session handling. That is a broad and difficult job, and the major PHP frameworks do it well.

What they deliberately do not solve is the shape of the domain layer. And that is the right call. A domain layer is project-specific in a way that cannot be prescribed generically. Which aggregates a project needs, how the bounded contexts get cut, whether domain events are processed synchronously or asynchronously, what invariants an aggregate protects: these are the domain decisions. A framework that prescribed them would end up either too rigid or too generic to be useful.

The consequence is the gap. Frameworks end at the controller. Business logic begins after the domain layer. And between them sits a piece that every team rebuilds. Each with their own conventions, their own directory layouts, their own interpretation of the repository pattern.

What the Gap Concretely Contains

What actually gets built during those weeks of foundation work? It is worth listing, because the list looks nearly identical in every PHP project.

  • Aggregate classes with typed attributes, constructors that check invariants, and methods that encapsulate state changes
  • Repositories with clearly separated query and persist paths that reconstitute an aggregate from the database or store it back
  • Command handlers with the recurring stages of initialization, validation, hydration, applying the change, and persisting
  • Query handlers for single fetches and paginated lists, often with separate read models
  • DTOs for inbound and outbound data, with mapping logic in both directions
  • Validators that do not just check form input, but enforce business rules before an aggregate is touched
  • Domain events that fire after successful operations and propagate to other bounded contexts or external systems
  • Bounded-context facades that protect the internals of a context from its neighbors and expose only well-defined operations
  • a Domain API derived from the use-case definitions: typed operations with fixed inputs and outputs that other contexts integrate against

Per aggregate that comes to forty or sixty files, all built in the same shape. Three to five aggregates in a mid-sized project add up to hundreds of files that must follow the same structural pattern, or the architecture fragments into many slightly different interpretations of the same concept.

The Cost of the Gap

These foundation weeks are not only a time problem. They are a consistency problem that amortizes across the lifetime of the project. Three effects recur, each visible in the codebases I have read over the past years.

First, drift. What was done cleanly in the first aggregate looks slightly different in the third. Whoever writes the fourth borrows from the second, which was already a compromise. Eighteen months in, the same codebase contains three different ways to hydrate an aggregate, two conventions for repository methods, and a growing set of edge cases that fit nowhere cleanly.

Second, onboarding. A new developer in a hand-built DDD project needs six to eight weeks before they can independently extend a bounded context. Not because the concepts are hard, but because every codebase has its own local variant of the concepts. Generic DDD knowledge is not enough; the project-specific pattern has to be inferred from reading code.

Third, maintenance. Pushing a schema change through a hand-built aggregate typically touches between fifteen and thirty files. The entity, repository methods, command DTOs, validators, API schemas, tests. Every one of these is a chance to forget something. A schema migration done wrong is not a single bug. It is a class of bugs.

From my observation across PHP projects, as an estimate and not a measurement, roughly half the development capacity in the first twelve months goes into this structural work. Not into shipping features, but into building the infrastructure that features are meant to ride on. Anyone who has watched that drift in their own codebase recognizes the symptom as gradual architecture erosion: a clean start, a quiet decline, and at some point a refactor that never happens.

Use Cases

Start Greenfield Projects Right. With DDD in PHP.

New project, a hundred open decisions. Jardis gives you production-ready DDD architecture on day 1, not a prototype that never gets cleaned up.

Learn more

The Layer Has a Name

If the gap exists and opens up in every complex PHP project, the natural question is whether it belongs in tooling. Not in the web framework, which has its own job, but in a separate layer that closes exactly this gap.

That is where Jardis sits in the stack:

HTTP Request
  → Web framework (Laravel, Symfony, Slim, choose freely)
    → Controller
      ::: Jardis layer :::
        → Domain → Bounded Context → Command/Query → Response
          → Business logic (Phase 3, hand-written)

Under Jardis sits the web framework, whichever one the team picked: Symfony, Laravel, Slim, a hand-rolled router over PSR-15. Jardis replaces none of it. Above Jardis sits the project-specific business logic, which is what makes the software unique. That also stays classic developer work.

What Jardis takes over is the layer in between: aggregates, repositories, command and query handlers, validation, domain events, bounded-context facades. From a schema definition, the builder produces the files a team would otherwise write by hand: the full forty to sixty files per aggregate, in the same structural pattern, identical every build.

This is the core of the claim "Start building the foundation of complex software in days, not months." The foundation of a complex project arrives in days rather than months, because the structural work comes from the schema definition rather than from repetitive typing.

Complement, Not Replacement

A boundary needs to be drawn explicitly here, because the question comes up in every conversation with PHP architects. Jardis does not compete with Symfony or Laravel. These frameworks solve the HTTP problem, the routing, the request handling, the dependency injection. They solve it well, and they sit on years of matured ecosystem work that no new platform should sensibly try to recreate.

What Jardis solves is a different job: the layer above the controller that the framework deliberately does not deliver. Both layers belong in the same stack. A team using Jardis keeps their Symfony or Laravel controller, calls from there into the Jardis-generated domain layer, and gets a response back that they ship to the client through the framework, as usual.

This separation is not just marketing courtesy. It is a technical consequence of hexagonal architecture: the domain core of a piece of software should know nothing about the outside world, neither the database nor the web framework it happens to run in. If the domain layer is framework-agnostic, any framework can sit underneath. That is the precondition for Jardis to exist as a separate layer without marrying any one framework.

In practice this means: an existing Symfony project can move aggregate by aggregate onto a Jardis-generated domain layer without switching frameworks. A greenfield project can pick Slim because the HTTP layer should be minimal, and use Jardis for everything above it. A team that already has Laravel in-house keeps the controllers there and pushes the domain into the Jardis layer. The framework remains the team's choice.

What Remains for the Team

The obvious worry with any proposal that pulls a layer out of day-to-day project work is a question of identity: what is left for the team. The answer is concrete: everything that makes the software unique.

Where do the bounded-context boundaries run, does Shipping belong to Sales, or is it its own context? Which invariants does the Order aggregate protect, can an order exist without a shipping address until payment clears? Which domain events trigger which downstream processes, when does invoicing run, when does warehouse reservation, when customer notification? These are the real architecture decisions, and they get made before the first builder run.

Phase 3, the hand-written business logic, lives parallel to the Jardis-generated code in its own directory. This is where the project- specific behavior lives: how a cancellation actually unwinds, which payment verification a specific marketplace needs, what data shape a concrete ERP integration requires. That work does not disappear. It becomes more visible, because the structural routine no longer covers it up.

Audiences

PHP Architecture That Enforces Itself

You define the architecture. The Jardis Builder enforces it at the filesystem level. Hexagonal architecture, CQRS, and Domain Events as generated structure, not as a convention nobody follows after sprint 10.

Learn more

How Jardis Approaches the Topic

The missing layer between controller and business logic is not a new problem. It is the reason a senior architect in a PHP project is needed for months before the first feature can ship. It is the reason half the development capacity in the first twelve months does not go into domain work. And it is the reason DDD has a reputation in the PHP world for being too expensive: not because the concepts are too complex, but because their implementation starts from scratch every time.

Jardis is the answer to that gap. A separate layer in the stack that sits between controller and business logic, produces the full domain architecture from schema definitions, and ensures the consistency of that architecture structurally, not by convention, but by the property that every aggregate comes from the same builder. The web framework stays free. The business logic stays with the team. The foundation of a complex project arrives in days, not months.

How the builder does this concretely, which files emerge from a schema definition, what the workflow from domain description to running code looks like: that is the topic of the next article in this series. Once the layer is understood as a real piece of the stack, the next question is how it actually gets produced.