Build Your Own PostgreSQL
Processes, memory, and the shared system
What You Will Learn
- Understand one service, many workers in a PostgreSQL-like database
- Understand private thought, shared reality in a PostgreSQL-like database
- Understand process map in a PostgreSQL-like database
- Understand why not one big loop? in a PostgreSQL-like database
One service, many workers
From the application side, there is only "the database": one host, one port, one name in a connection string. Inside, it is a society with rules. Client work happens in backend processes. Long-running maintenance happens in background workers. Shared memory holds the structures that many participants must see. Files on disk outlive them all. The design problem is not simply how to run queries. It is how to let many independent workers cooperate without trusting any one worker too much.
Postgre-style architecture has historically favored a process-per-connection model. When a client connects, the server gives that session a backend process. That backend parses messages, runs transactions, executes queries, and returns results. The operating system isolates one process from another, which is a useful safety boundary. A crash in one backend is less likely to corrupt the private memory of another. The cost is that coordination must happen through shared structures, interprocess signaling, and carefully managed locks.
Private thought, shared reality
Shared memory is where the separate workers become one database. It holds the buffer pool, lock tables, process arrays, transaction status structures, and other coordination points. Private memory is where a backend keeps session-local state, temporary plan data, expression evaluation context, and intermediate results. This split matters. Private memory can be discarded when a session dies. Shared memory must be protected because it carries the live coordination state of the whole server.
There are also background processes whose job is not to answer a single client. A checkpointer nudges dirty data toward disk so crash recovery has a bounded starting point. A writer can smooth out buffer flushing. A vacuum worker reclaims space made obsolete by old tuple versions. A WAL writer helps move log records to durable storage. Replication senders stream change to standbys. These workers keep the database responsive by moving housekeeping out of the critical path when possible.
Process map
Why not one big loop?
The contrasting design is a single-threaded database loop. It accepts a request, runs it, then moves to the next. This design is easy to reason about because there is no true concurrent mutation inside the engine. It can work well for embedded databases or for workloads where simplicity matters more than parallel client throughput. But a server database must tolerate many clients waiting on disks, networks, locks, and each other. One blocked request cannot freeze the entire world.
Another option is a thread-per-connection design. Threads share an address space by default, which can make communication cheaper. It can also make memory safety failures more dangerous, because a bad pointer or buffer overwrite can damage the whole process. A process model pushes some isolation down to the operating system. That choice shapes everything above it: process startup cost, connection pooling needs, shared memory layout, and how failures are contained.
The memory story has its own tradeoffs. If every backend cached its own pages privately, repeated reads could be fast within one session, but the server would waste memory and struggle to coordinate writes. If all memory were shared, every small allocation would need global discipline. Postgre-like systems use both. Shared memory is reserved for data and coordination that must cross sessions. Private memory gives each backend room to work without turning every expression evaluation into a shared-memory negotiation.
Coordination has different speeds
Locks and latches make this society livable. A lock is usually about logical permission: may this transaction update this table, or must it wait? A latch or lightweight synchronization primitive is often about protecting an internal structure for a very short time: may this process adjust this buffer header right now? Mixing those concepts leads to systems that are either unsafe or slow. The architecture has to distinguish human-visible conflicts from internal critical sections.
The process array is a core part of the design. It records which backends exist, what transactions they are running, and what snapshots might still matter. MVCC, vacuum, lock waits, and cancellation all depend on that information. A database cannot clean up old row versions merely because a newer one exists. It must know whether any active reader could still need the older view.
Good server architecture also accepts that failure is normal. A backend may be cancelled. A client may disconnect. A background worker may restart. The postmaster, or supervising parent process, keeps the system coherent when children come and go. Startup and shutdown are therefore architectural events, not just operational details. The server must initialize shared memory, launch essential workers, accept clients, and drain or terminate work in controlled ways.
Into the client conversation
This layer is what lets the database serve many users at once, hold shared state, and move maintenance through background work. The next question is how a client conversation begins. Before a query becomes a plan, it first crosses the wire as messages with order, identity, and security boundaries.