You know that moment very early on in a project? You are building a system with complex business rules and long-lived data, maybe a subscription platform or a contract management system. You make a guess about what you will need someday, and before long, you have several databases, a message queue, a streaming platform, and a cache. Each piece makes sense when you add it, but over time, that complexity stacks up and becomes its own burden.
What if we asked a different question? Instead of “what could we need later?”, try asking: “what do we actually need right now to solve this problem safely, clearly, and for the long term?”
Let’s start with design, not tech.
EventModeling shifts your focus. You model behavior first, before you ever touch code. The outcome is an EventModel, a blueprint describing system behavior through events over time. It breaks the system into vertical slices, coherent parts of the business flow, where only a handful of patterns appear again and again: state changes, state views, automations, and translations.
This is actually a form of constraint. By limiting the patterns and arranging them along clear slices, you avoid unnecessary variation. The system becomes structurally simple long before you build it.
When you move from that model to implementation, the architecture follows naturally.
Vertical slice architecture mirrors the EventModel. You can often skip traditional aggregates and query the event history directly through Dynamic Context Boundaries. With CQRS, reads and writes split apart. Projections handle all queries, while the event store focuses exclusively on validating business rules during commands.
Here is the interesting bit. Because the architecture stays close to the model, and responsibilities stay clear, you often find you do not need all that extra infrastructure you thought you did.
In fact, a single PostgreSQL database is often enough.
Here is what that looks like in practice.
A simple event store table:
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
recorded_at TIMESTAMP NOT NULL DEFAULT now(),
identifiers JSONB NOT NULL,
payload JSONB NOT NULL
);
Notice the split between identifiers and payload. Identifiers carry only the fields you query by, like correlation IDs or business keys. Payload holds the full domain data. This lets you index exactly what you need:
CREATE INDEX idx_events_identifiers
ON events USING GIN (identifiers jsonb_path_ops);
The index stays small and fast. Queries are snappy, and write performance holds steady.
From there, validating business rules is just expressing intent against the event history.
Check if a contract was cancelled:
SELECT EXISTS (
SELECT 1
FROM events
WHERE identifiers @> '{"contractId": "C-123"}'
AND event_type = 'ContractCancelled'
);
Or grab the latest state transition:
SELECT *
FROM events
WHERE identifiers @> '{"contractId": "C-123"}'
AND event_type IN ('ContractActivated', 'ContractSuspended')
ORDER BY id DESC
LIMIT 1;
These queries are not random. They map directly to Dynamic Context Boundaries from the EventModel. Your queries reflect your design.
Scalability often surprises people here.
With moderate throughput, well under a thousand writes per second, PostgreSQL handles it comfortably. With partitioning, you can store billions of events and still perform reasonably. Since projections handle all read-heavy work, the event store stays focused and predictable.
The real enablers are not fancy tools. They are disciplined habits: partition data over time, index only what you must, and keep queries tight and deterministic.
The payoff is not just technical. The whole system changes character.
The infrastructure surface shrinks. Less to operate, fewer failure modes. Costs follow a linear path rather than skyrocketing with each new component. Most importantly, the architecture stays visible. Your system reflects the business logic: the events, the flows, the boundaries. It is not buried under layers of infrastructure.
To be fair, this is not a universal solution. If you are pushing very high throughput, real-time streaming, or massive-scale event distribution, you will want specialized tools.
But that is not every system. Far from it.
The real insight is subtle.
The simplicity does not come from picking PostgreSQL. It comes from the choices you make before that: start with a clear EventModel, structure it into well-defined slices, apply a small, consistent set of patterns. Only then does the architecture become obvious and the infrastructure minimal.
Without that foundation, no database, however simple or sophisticated, will save you.
Complexity happens. Sometimes you cannot avoid it. But complexity you introduce too early has a habit of sticking around, shaping the system long after the original reasons are gone.
Simplicity, applied deliberately at the design level, behaves differently. It scales quietly, often further than you expect, and leaves breathing room for the system to evolve without fighting its own weight.
Simplicity is not the finish line. It is where you start.
Originally published on LinkedIn (2026-05-06): https://www.linkedin.com/pulse/how-far-can-you-actually-get-just-postgresql-gabriel-n-schenker-1yxde