The Second Pattern: The Ones I Left Out
Five patterns I know well, that come up constantly, and would not reach for in PHP. Not because they are bad ideas, but because of what the runtime does and does not give you.
Nine articles arguing for patterns is a genre, and the genre has a problem. If every article concludes that you should adopt the thing it is about, then the articles are not analysis, they are advocacy with code samples, and you have no way of telling which recommendations I actually believe.
So here are five patterns I know reasonably well, that come up constantly in architecture discussions, and that I would not reach for in a PHP application. None of them is a bad idea. Most are excellent ideas somewhere else, which is rather the point.
The test I have been applying throughout, stated plainly: a pattern earns its keep when the cost of adopting it is smaller than the cost of the problem it solves, in the environment you are actually in. That last clause is where most architecture advice falls over, because patterns get transplanted between runtimes whose properties differ enormously and the transplant is rarely examined.
For PHP the property that matters most is shared-nothing execution. Each request gets a fresh process, builds the world, does the work and throws everything away. That single fact is behind more of what follows than anything else, and it is worth holding onto as you read.
Event Sourcing
The promise is genuinely appealing. Stop storing current state, store the events that produced it, and you get a complete audit trail, the ability to answer questions about the past you did not think to ask at the time, and read models you can rebuild whenever you like.
Everything in that sentence is true. It is also a much larger commitment than it appears from a conference talk.
You need an event store, and it has to give you ordering and optimistic concurrency per stream. You need projections, and something to rebuild them, which for a system of any size means a rebuild that takes hours and has to run without downtime. You need event versioning, because your events are now a schema you can never change, only extend, which means upcasting old events into new shapes and maintaining those upcasters forever. And you need snapshots, because replaying four thousand events to answer one question does not survive contact with a shared-nothing runtime that rebuilds state on every request.
None of that is impossible in PHP, and EventSauce and Broadway are good libraries written by people who understand this properly. The question is not whether it can be done. It is what fraction of applications need what it buys.
In my experience the honest answer is that most teams reaching for event sourcing want an audit log. They want to know who changed what and when, and to be able to show somebody. An append-only table of domain events written alongside your normal state, in the same transaction, gives you that for a day’s work and costs you nothing structurally. You keep your current-state tables, your queries stay ordinary, and you can still answer historical questions.
The version worth the full price is where the events genuinely are the truth rather than a record of changes to the truth. Ledgers. Anything where the balance is definitionally the sum of the transactions and where you will be asked to prove it. In that world the audit trail is not a feature you added, it is the model, and event sourcing stops being expensive and starts being the simplest thing that works.
On Tempest specifically there is no event store, and the event bus is synchronous and in-process rather than a durable log. You would be building the infrastructure yourself, on top of a database component that is currently marked experimental. That is a big project sitting underneath your actual project.
Entity-Boundary-Interactor and its descendants
Ivar Jacobson described Entity, Boundary and Interactor in the early nineties, and most of what people call Clean Architecture or Hexagonal Architecture is a rediscovery of it. Entities hold enterprise rules, interactors hold application rules, boundaries define how anything outside gets in, and dependencies point inward so the domain knows nothing of the delivery mechanism.
I have a lot of respect for the idea. The reason I would not build it in Tempest is specific, and it is not the usual complaint about boilerplate.
The dependency inversion exists to protect your domain from the framework. That threat is real in frameworks where the framework is inside your objects, where your entity extends a base class that knows about the database, where a static call reaches into a service locator from the middle of your business logic. Under those conditions the ceremony buys you something, because without it there is no boundary at all.
Tempest has already inverted what Clean Architecture inverts. Models are plain
objects with public typed properties, implementing nothing. There are no facades,
and although a Tempest\Container\get() helper exists, nothing obliges your
classes to reach for it, so dependencies arrive through constructors as a matter
of course. The framework finds your code through
discovery rather than requiring your code to register itself. Your domain classes
are already framework-independent, because there was never a mechanism by which
the framework got into them.
So the layer of DTOs protecting your entities from the ORM is protecting them from something that is not there. You will write mapping code between two shapes that are already identical, and every new field will be added in four places by somebody who has stopped asking why.
Where I would still build it is when there really are multiple delivery mechanisms driving the same logic, with genuinely different needs. An HTTP API, a console tool and a queue consumer all invoking one use case is the case Jacobson was describing, and an explicit interactor with its own request and response shapes is the right answer. Notice that the justification is the multiplicity, not the framework, and that a great many applications have exactly one delivery mechanism.
The Repository, as an abstraction over the ORM
The other four on this list are exotic. This one is not, and leaving it out would be dodging the question, because it is the pattern PHP developers actually reach for on a Tuesday.
The promise is that an interface between your domain and your persistence lets you swap the database, and makes the domain testable without one.
The swap does not happen. I have never seen a team exercise that option, and if you are honest about the odds you are paying an ongoing cost for an option you will not use. That is a reasonable thing to buy if the premium is low. It is not low here.
The interface ends up shaped like the queries anyway, which is the real problem.
You start with findById, then findByCarrier, then findByCarrierAndStatus,
and within a year the interface has nineteen methods and every new screen adds
one. That is the nine-parameter method from article one, arriving one level up,
and no amount of interface makes it composable. The specification in that article
solves it precisely because it is not a method on a repository.
The testability claim needs care too. An in-memory implementation for tests only helps if it behaves like the real one, and it does not. Null ordering, collation, case sensitivity, transaction visibility and unique constraint behaviour all differ, so your suite passes against an array and fails against Postgres. Substituting a fake for a database mostly tests that your fake agrees with itself.
Tempest removes the remaining motivation. Models are plain objects that implement
nothing, so there is no ORM base class to hide from. query(Model::class) is
already a seam you can stand behind. And QueryScope gives you named, reusable,
composable query logic without an interface method per question.
There are two cases where I would still write one. When there genuinely are multiple backends, a database and a search index answering the same question, an interface is describing something real. And when the repository is an anti-corruption layer over somebody else’s API, which is article two rather than this pattern, and worth doing for entirely different reasons.
What I would not do is add an interface with one implementation, named after a pattern, on the grounds that it is good practice.
Data, Context and Interaction
DCI is the one I most wish worked, because the problem it identifies is real. Object-oriented code is good at describing what things are and bad at describing what happens. Read a well-factored domain model and you can see the nouns perfectly, while the story of a consignment being booked is scattered across eleven classes and reconstructing it is an afternoon.
Trygve Reenskaug’s answer, with James Coplien, was to separate what an object is from the roles it plays. Data objects are dumb and stable. A context represents a use case, and for the duration of that use case it binds roles onto the data objects, so the objects genuinely gain the behaviour of the role while they are playing it. Read a context and you read the use case, in one place, in order.
The mechanism does not fit PHP, and the reason is concrete rather than aesthetic. DCI wants runtime role injection: attaching behaviour to an existing object, temporarily, without changing its class. PHP has no way to do this. Traits are resolved at compile time and there is no runtime equivalent, and there is no object reclassification.
The workarounds each break something you need. Wrapping the object in a decorator
gives you a different object, so identity comparison stops working and anything
holding the original does not see the role. Routing role methods through
__call gets you the behaviour and costs you static analysis, which in a
codebase of any size is a poor exchange. Neither is DCI. Both are impressions of
it that leak.
What I take from DCI instead is the diagnosis. If you cannot find the use case in your code, that is a real problem and worth solving, and article seven is one way to solve it: a process manager puts the sequence in one readable place without needing the language to support role binding.
The Actor Model
Actors are a genuinely great concurrency model. Independent entities, each owning its own state, communicating only by message, processing one message at a time so that there is no shared mutable state to get wrong. Erlang built decades of telecoms reliability on it.
Everything about it assumes a runtime PHP does not have. An actor is long-lived, addressable, stateful and concurrent with other actors. PHP’s request lifecycle gives you a process that starts, does one thing and exits, holding no state between requests. There is nowhere for an actor to live.
You can obtain the missing runtime. Swoole, ReactPHP, AmPHP and Fibers all get you closer, and people run real systems this way. But look carefully at what you trade. PHP’s operational simplicity comes almost entirely from that shared-nothing model: a request that leaks memory or segfaults takes down one process, and the next request is unaffected. Move to long-lived workers holding actor state and a leak is now cumulative, a fatal takes out everything that worker was holding, and your deployment story acquires questions about draining and state migration that it did not have before.
That trade can be worth making. A websocket gateway, a game server, a long-running simulation are all better with a persistent runtime, and if you are already running Swoole for other reasons the marginal cost is small. What I would not do is adopt an actor-shaped design inside a normal request-response application, put actors in classes that are constructed and destroyed within a single request, and call it the actor model. You get the vocabulary and none of the properties, which is worse than not having tried.
Tempest, for what it is worth, does not push against this. Its process package wraps Symfony’s, so concurrency means operating system processes rather than in-process actors, and the async command bus spawns child processes. That is the grain of the runtime, and it is a reasonable grain.
What this list is and is not
None of these are on the list because they are complicated. Several of the patterns I did recommend are more complicated than DCI, and the Blackboard in article nine is more obscure than any of them.
They are here because of a mismatch between what the pattern needs and what PHP provides. Event sourcing needs an infrastructure investment most applications will not repay. Clean Architecture needs a framework worth defending against, and Tempest is not one. The repository needs a persistence swap that never comes. DCI needs runtime role binding the language does not have. And the actor model needs a runtime whose absence is the source of PHP’s operational simplicity.
Most of those are facts about the environment rather than about the patterns, and the same ideas evaluated for Elixir or the JVM would come out differently. That is what I meant at the start about transplanting patterns between runtimes without examining the transplant.
What the other nine were for
Every article in this series answered the same question in a different costume: under what conditions does this pay. That question only means something if the answer is sometimes no.
What I would most like to survive the series is the habit rather than any of the patterns. When somebody proposes an architecture, ask what specifically goes wrong without it, what it costs to maintain in your environment, and how you would know afterwards whether it was worth it. Most proposals do not survive the second question and almost none survive the third.
The patterns are just the vocabulary. Knowing when not to use them is the part that took me longest to learn, and it is the only part I would defend.
The Second Pattern
You are reading Part 10 of 10 in this learning series.
Keep Reading
The Second Pattern: Blackboard
A pipeline works until one step both needs and improves the same piece of information. That is a cycle, and a topological sort has exactly one contract: there are no cycles.
Sept 2026 · 12 min read
PHPThe Second Pattern: Event-Carried State Transfer
A consumer that receives an ID and immediately asks you for the record has not been decoupled from you. It has been given a slightly slower way to call your API.
Sept 2026 · 12 min read
PHPThe Second Pattern: Process Manager
Everyone calls it a Saga. Most of the time it is a Process Manager, and the muddle is why teams adopt the word and then never write the compensations it never promised them.
Sept 2026 · 14 min read