Event sourcing in PHP without the framework tax.
Most teams reach for a heavyweight framework the moment someone says "event sourcing," and quietly inherit its entire worldview along with it. You can get the guarantees you actually want — append-only truth, replayable state, an audit trail that is the product — with far less ceremony than the ecosystem suggests.
This essay is the version I wish someone had handed me before my first production system. It assumes you know why you want events; it's about keeping the machinery honest.
This is written for PHP teams, but nothing here is language-specific. The Go version has fewer moving parts and the same failure modes.
The ledger is the point
An event store is an append-only log. That single constraint — you may add, never mutate — is where every downstream guarantee comes from. When "what happened" is more valuable than "what is," you keep the history and derive the state.1
The state you show users is a projection: a fold over the log. Delete it, replay the log, get it back. That property is worth more than most feature work.
An event store you can't replay confidently is just a slower database with worse ergonomics.
Projections, not tables
Treat each read model as disposable. The moment a projection becomes irreplaceable, you've reintroduced the mutable-state problem you were trying to escape. Here's a minimal projector:
final class OrderProjector {public function apply(Event $e): void {match($e::class) {OrderPlaced::class => $this->insert($e),OrderShipped::class => $this->markShipped($e),default => /* ignore */ null,};}}
Note the default => null. A projector that throws on unknown events can't survive schema evolution. Ignore what you don't understand yet.
Versioning without tears
Events are forever, so their shape is a contract. Three rules keep that contract cheap:
- Only ever add optional fields. Never remove or repurpose.
- When a real change is unavoidable, write a new event type and an upcaster that maps old to new on read.
- Keep serialization boring — explicit, versioned, and tested against fixtures of real historical payloads.
The first time you replay two years of production events into a new projection and the numbers match the old system to the cent — that's the moment the discipline pays for itself.
Snapshots are a cache, nothing more
Replaying from the beginning gets slow. Snapshots fix that, but treat them strictly as a cache: deletable, rebuildable, never a source of truth. If a bug means you can’t trust a snapshot, you throw it away and replay.
Where it stops being worth it
Event sourcing is a cost. You pay in tooling, onboarding, and a mental model most engineers haven't internalized. It pays back only where history has value. A comparison I keep on hand:
| Concern | CRUD | Event-sourced |
|---|---|---|
| Audit trail | Bolted on | Free |
| Read models | One | Many, disposable |
| Onboarding cost | Low | High |
| Debugging past state | Guesswork | Replay |
If the top two rows don't matter to your domain, don't do this. Use a database and get on with your life.2
Closing
Keep the log honest, keep projections disposable, keep snapshots as cache. Do that and event sourcing stops being a framework you serve and becomes a property your system has. That’s the whole trick.