Back to Main
Learn System Design
Question Breakdowns
Advanced Topics
Key Technologies
PostgreSQL
Learn when and how to use PostgreSQL in your system design interviews
There's a good chance you'll find yourself discussing PostgreSQL in your system design interview. After all, it's consistently ranked as the most beloved database in Stack Overflow's developer survey and is used by companies from Reddit to Instagram and even the very website you're reading right now.
That said, it's important to understand that while PostgreSQL is packed with features and capabilities, your interviewer isn't looking for a database administrator. They want to see that you can make informed architectural decisions. When should you choose PostgreSQL? When should you look elsewhere? What are the key trade-offs to consider?
I often see candidates get tripped up here. They either dive too deep into PostgreSQL internals (talking about MVCC and WAL when the interviewer just wants to know if it can handle their data relationships), or they make overly broad statements like "NoSQL scales better than PostgreSQL" without understanding the nuances.
In this deep dive, we'll focus specifically on what you need to know about PostgreSQL for system design interviews. We'll start with a practical example, explore the key capabilities and limits that should inform your choices, and build up to common interview scenarios.
For this deep dive, we're going to assume you have a basic understanding of SQL. If you don't, I've added an Appendix: Basic SQL Concepts at the end of this page for you to review.
Let's get started.
A Motivating Example
Let's build up our intuition about PostgreSQL through a concrete example. Imagine we're designing a social media platform - not a massive one like Facebook, but one that's growing and needs a solid foundation.
Our platform needs to handle some fundamental relationships:
- Users can create posts
- Users can comment on posts
- Users can follow other users
- Users can like both posts and comments
- Users can create direct messages (DMs) with other users
What makes this interesting from a database perspective? Well, different operations have different requirements:
- Multi-step operations like creating DM threads need to be atomic (creating the thread, adding participants, and storing the first message must happen together)
- Comment and follow relationships need referential integrity (you can't have a comment without a valid post or follow a non-existent user)
- Like counts can be eventually consistent (it's not critical if it takes a few seconds to update)
- When someone requests a user's profile, we need to efficiently fetch their recent posts, follower count, and other metadata
- Users need to be able to search through posts and find other users
- As our platform grows, we'll need to handle more data and more complex queries
This combination of requirements - complex relationships, mixed consistency needs, search capabilities, and room for growth - makes it a perfect example for exploring PostgreSQL's strengths and limitations. Throughout this deep dive, we'll keep coming back to this example to ground our discussion in practical terms.
Core Capabilities & Limitations
With a motivating example in place, let's dive into what PostgreSQL can and can't do well. Most system design discussions about PostgreSQL will center around its read performance, write capabilities, consistency guarantees, and schema flexibility. Understanding these core characteristics will help you make informed decisions about when to use PostgreSQL in your design.
Read Performance
First up is read performance - this is critical because in most applications, reads vastly outnumber writes. In our social media example, users spend far more time browsing posts and profiles than they do creating content.
When a user views a profile, we need to efficiently fetch all posts by that user. Without proper indexing, PostgreSQL would need to scan every row in the posts table to find matching posts - a process that gets increasingly expensive as our data grows. This is where indexes come in. By creating an index on the user_id column of our posts table, we can quickly locate all posts for a given user without scanning the entire table.
Basic Indexing
The most fundamental way to speed up reads in PostgreSQL is through indexes. By default, PostgreSQL uses B-tree indexes, which work great for:
- Exact matches (WHERE email = 'user@example.com')
- Range queries (WHERE created_at > '2024-01-01')
- Sorting (ORDER BY username if the ORDER BY column match the index columns' order)
By default, PostgreSQL will create a B-tree index on your primary key column, but you also have the ability to create indexes on other columns as well.
Beyond Basic Indexes
Where PostgreSQL really shines is its support for specialized indexes. These come up frequently in system design interviews because they can eliminate the need for separate specialized databases:
Full-Text Search using GIN indexes. Postgres supports full-text search out of the box using GIN (Generalized Inverted Index) indexes. GIN indexes work like the index at the back of a book - they store a mapping of each word to all the locations where it appears. This makes them perfect for full-text search where you need to quickly find documents containing specific words:
For many applications, this built-in search capability means you don't need a separate Elasticsearch cluster. It supports everything from:
- Word stemming (finding/find/finds all match)
- Relevance ranking
- Multiple languages
- Complex queries with AND/OR/NOT
JSONB columns with GIN indexes are particularly useful when you need flexible metadata on your posts. For example, in our social media platform, each post might have different attributes like location, mentioned users, hashtags, or attached media. Rather than creating separate columns for each possibility, we can store this in a JSONB column (giving us the flexibility to add new attributes as needed just like we would in a NoSQL database!).
Geospatial Search with PostGIS. While not built into PostgreSQL core, the PostGIS extension adds powerful spatial capabilities. Just like we can index text for fast searching, PostGIS lets us index location data for efficient geospatial queries. This is perfect for our social media platform when we want to show users posts from their local area:
PostGIS is incredibly powerful - it can handle:
- Different types of spatial data (points, lines, polygons)
- Various distance calculations (as-the-crow-flies, driving distance)
- Spatial operations (intersections, containment)
- Different coordinate systems
The index type used here (GIST) is specifically optimized for geometric data, using R-tree indexing under the hood. This means queries like "find all posts within X kilometers" or "find posts inside this boundary" can be executed efficiently without having to check every single row.
Better yet, we can combine all these capabilities to create rich search experiences. For example, we can find all video posts within 5km of San Francisco that mention "food" in their content and are tagged with "restaurant":
Let me rewrite this section to better explain the why and connect the concepts:
Query Optimization Essentials
So far we've covered the different types of indexes PostgreSQL offers, but there's more to query optimization than just picking the right index type. Let's look at some advanced indexing strategies that can dramatically improve read performance.
Covering Indexes
When PostgreSQL uses an index to find a row, it typically needs to do two things:
- Look up the value in the index to find the row's location
- Fetch the actual row from the table to get other columns you need
But what if we could store all the data we need right in the index itself? That's what covering indexes do:
Partial Indexes
Sometimes you only need to index a subset of your data. For example, in our social media platform, most queries are probably looking for active users, not deleted ones:
Partial indexes are particularly effective in scenarios where most of your queries only need a subset of rows, when you have many "inactive" or "deleted" records that don't need to be indexed, or when you want to reduce the overall size and maintenance overhead of your indexes. By only indexing the relevant subset of data, partial indexes can significantly improve both query performance and resource utilization.
Practical Performance Limits
There is a good chance that during your non-functional requirements you outlined some latency goals. Ideally, you even quantified them! That means that as you go deep into the design, you need some basic performance numbers in mind. These numbers are very rough estimates as real numbers depend heavily on the hardware and the specific workload. That said, estimates should be enough to get you started in an interview.
- Query Performance:
- Simple indexed lookups: tens of thousands per second per core
- Complex joins: thousands per second
- Full-table scans: depends heavily on whether data fits in memory
- Scale Limits:
- Tables start getting unwieldy past 100M rows
- Full-text search works well up to tens of millions of documents
- Complex joins become challenging with tables >10M rows
- Performance drops significantly when working set exceeds available RAM
Keep in mind, memory is king when it comes to performance! Queries that can be satisfied from memory are orders of magnitude faster than those requiring disk access. As a rule of thumb, you should try to keep your working set (frequently accessed data) in RAM for optimal performance.
Write Performance
Now let's talk about writes. While reads might dominate most workloads, write performance is often more critical because it directly impacts user experience - nobody wants to wait seconds after hitting "Post" for their content to appear.
When a write occurs in PostgreSQL, several steps happen to ensure both performance and durability:
- Transaction Log (WAL) Write [Disk]: Changes are first written to the Write-Ahead Log (WAL) on disk. This is a sequential write operation, making it relatively fast. The WAL is critical for durability - once changes are written here, the transaction is considered durable because even if the server crashes, PostgreSQL can recover the changes from the WAL.
- Buffer Cache Update [Memory]: Changes are made to the data pages in PostgreSQL's shared buffer cache, where the actual tables and indexes live in memory. When pages are modified, they're marked as "dirty" to indicate they need to be written to disk eventually.
- Background Writer [Memory → Disk]: Dirty pages in memory are periodically written to the actual data files on disk. This happens asynchronously through the background writer, when memory pressure gets too high, or when a checkpoint occurs. This delayed write strategy allows PostgreSQL to batch multiple changes together for better performance.
- Index Updates [Memory & Disk]: Each index needs to be updated to reflect the changes. Like table data, index changes also go through the WAL for durability. This is why having many indexes can significantly slow down writes - each index requires additional WAL entries and memory updates.
The practical implication of this design is that write performance is typically bounded by how fast you can write to the WAL (disk I/O), how many indexes need to be updated, and how much memory is available for the buffer cache.
Throughput Limitations
Now we know about what happens when a write occurs in PostgreSQL, before we go onto optimizations, let's first talk about the practical limits of write throughput. This is important to know as it will help you decide whether PostgreSQL is a good fit for your system.
A well-tuned PostgreSQL instance on good (not great) hardware can handle:
- Simple inserts: ~5,000 per second per core
- Updates with index modifications: ~1,000-2,000 per second per core
- Complex transactions (multiple tables/indexes): Hundreds per second
- Bulk operations: Tens of thousands of rows per second
What affects these limits? Several factors:
- Hardware: Write throughput is often bottlenecked by disk I/O for the WAL
- Indexes: Each additional index reduces write throughput
- Replication: If configured, synchronous replication adds latency as we wait for replicas to confirm
- Transaction Complexity: More tables or indexes touched = slower transactions
Let me continue from there, building on our social media example:
Write Performance Optimizations
Ok, a single node can handle around 5k writes per second, so what can we do? How can we improve our write performance if we require more than that? Let's look at strategies ranging from simple optimizations to architectural changes.
We have a few options:
- Batch Processing
- Vertical Scaling
- Write Offloading
- Table Partitioning
- Sharding
Let's discuss each of these in turn.
1. Vertical Scaling
Before jumping to complex solutions, we can always consider just upgrading our hardware. This could mean using faster NVMe disks for better WAL performance, adding more RAM to increase the buffer cache size, or upgrading to CPUs with more cores to handle parallel operations more effectively.
This usually isn't the most compelling solution in an interview, but it's a good place to start.
2. Batch Processing
The simplest optimization is to batch writes together. Instead of processing each write individually, we collect multiple operations and execute them in a single transaction. For example, instead of inserting 1000 likes one at a time, we can insert them all in a single transaction. This means we're buffering writes in our server's memory before committing them to disk. The risk here is clear, if we crash in the middle of a batch, we'll lose all the writes in that batch.
3. Write Offloading
Some writes don't need to happen synchronously. For example, analytics data, activity logs, or aggregated metrics can often be processed asynchronously. Instead of writing directly to PostgreSQL, we can:
- Send writes to a message queue (like Kafka)
- Have background workers process these queued writes in batches
- Optionally maintain a separate analytics database
This pattern works especially well for handling activity logging, analytics events, metrics aggregation, and non-critical updates like "last seen" timestamps. These types of writes don't need to happen immediately and can be processed in the background without impacting the core user experience.
4. Table Partitioning
For large tables, partitioning can improve both read and write performance by splitting data across multiple physical tables. The most common use case is time-based partitioning. Going back to our social media example, let's say we have a posts table that grows by millions of rows per month:
Why does this help writes? First, different database sessions can write to different partitions simultaneously, increasing concurrency. Second, when data is inserted, index updates only need to happen on the relevant partition rather than the entire table. Finally, bulk loading operations can be performed partition by partition, making it easier to load large amounts of data efficiently.
Conveniently, it also helps with reads. When users view recent posts, PostgreSQL only needs to scan the recent partitions. No need to wade through years of historical data.
This is a great strategy for data that has a natural lifecycle like social media posts where new ones are most relevant.
5. Sharding
This is the most common solution in an interview. When a single node isn't enough, sharding lets you distribute writes across multiple PostgreSQL instances. You'll just want to be clear about what you're sharding on and how you're distributing the data.
For example, we may consider sharing our posts by user_id. This way, all the data for a user lives on a single shard. This is important, because when we go to read the data we want to avoid cross-shard queries where we need to scatter-gather data from multiple shards.
You typically want to shard on the column that you're querying by most often. So if we typically query for all posts from a given user, we'll shard by user_id.
Unlike DynamoDB, PostgreSQL doesn't have a built-in sharding solution. You'll need to implement sharding manually, which can be a bit of a challenge. Alternatively, you can use managed services like Citus which handles many of the sharding complexities for you.
Replication
While we've discussed how to optimize write performance on a single node, most real-world deployments use replication for two key purposes:
- Scaling reads by distributing queries across replicas
- Providing high availability in case of node failures
Replication is the process of copying data from one database to one or more other databases. This is a key part of PostgreSQL's scalability and availability story.
PostgreSQL supports two main types of replication: synchronous and asynchronous. In synchronous replication, the primary waits for acknowledgment from replicas before confirming the write to the client. With asynchronous replication, the primary confirms the write to the client immediately and replicates changes to replicas in the background. While the technical details may not come up in an interview, understanding these tradeoffs is important - synchronous replication provides stronger consistency but higher latency, while asynchronous replication offers better performance but potential inconsistency between replicas.
Scaling reads
The most common use for replication is to scale read performance. By creating read replicas, you can distribute read queries across multiple database instances while sending all writes to the primary. This is particularly effective because most applications are read-heavy.
Let's go back to our social media example. When users browse their feed or view profiles, these are all read operations that can be handled by any replica. Only when they create posts or update their profile do we need to use the primary. Now we have multiplied our read throughput by N where N is the number of replicas.
High Availability
The second major benefit of replication is high availability. By maintaining copies of your data across multiple nodes, you can handle hardware failures without downtime. If your primary node fails, one of the replicas can be promoted to become the new primary.
This failover process typically involves:
- Detecting that the primary is down
- Promoting a replica to primary
- Updating connection information
- Repointing applications to the new primary
Most teams use managed PostgreSQL services (like AWS RDS or GCP Cloud SQL) that handle the complexities of failover automatically. In your interview, it's enough to know that failover is possible and roughly how it works - you don't need to get into the details of how to configure it manually.
Data Consistency
If you've chosen to prioritize consistency over availability in your non-functional requirements, then PostgreSQL is a strong choice. It's built from the ground up to provide strong consistency guarantees through ACID transactions. However, simply choosing PostgreSQL isn't enough - you need to understand how to actually achieve the consistency your system requires.
Transactions
One of the most common points of discussion in interviews ends up being around transactions. A transaction is a set of operations that are executed together and must either all succeed or all fail together. This is the foundation for ensuring consistency in PostgreSQL.
Let's consider a simple example where we need to transfer money between two bank accounts. We need to ensure that if we deduct money from one account, it must be added to the other account. Neither operation can happen in isolation:
This transaction ensures atomicity - either both updates happen or neither does. However, transactions alone don't ensure consistency in all scenarios, particularly when multiple transactions are happening concurrently.
Transactions and Concurrent Operations
Transactions ensure consistency for a single series of operations, but things get more complicated when multiple transactions are happening concurrently. Remember, in most real applications, you'll have multiple users or services trying to read and modify data at the same time.
This is where many candidates get tripped up in interviews. They understand basic transactions but haven't thought through how to maintain consistency when multiple operations are happening simultaneously.
Let's look at an auction system as an example. Here users place bids on items and we accept bids only if they're higher than the current max bid. A single transaction can ensure that checking the current bid and placing a new bid happen atomically, but what happens when two users try to bid at the same time?
Here's how this could lead to an inconsistent state:
- User A's transaction reads current max bid: $90
- User B's transaction reads current max bid: $90
- User A places bid for $100
- User A commits
- User B places bid for $95
- User B commits
Now we have an invalid state: a $95 bid was accepted after a $100 bid!
There are two main ways we can solve this concurrency issue:
1. Row-Level Locking
The simplest solution is to lock the auction row while we're checking and updating bids. By using the FOR UPDATE clause, we tell PostgreSQL to lock the rows we're reading. Other transactions trying to read these rows with FOR UPDATE will have to wait until our transaction completes. This ensures we have a consistent view of the data while making our changes.
So when you need to ensure that two operations happen atomically, you'll want to emphasize to your interviewer how you will achieve that beyond simply saying "we'll use transactions". Instead, "we'll use transactions and row-level locking on the auction row", for this case.
2. Higher Isolation Level
Alternatively, we can use a stricter isolation level:
PostgreSQL supports three isolation levels, each providing different consistency guarantees:
- Read Committed (Default) is PostgreSQL's default isolation level that only sees data that was committed before the query began. As transactions execute, each query within a transaction can see new commits made by other transactions that completed after the transaction started. While this provides good performance, it can lead to non-repeatable reads where the same query returns different results within a transaction.
- Repeatable Read in PostgreSQL provides stronger guarantees than the SQL standard requires. It creates a consistent snapshot of the data as of the start of the transaction, and unlike other databases, PostgreSQL's implementation prevents both non-repeatable reads AND phantom reads. This means not only will the same query return the same results within a transaction, but no new rows will appear that match your query conditions - even if other transactions commit such rows.
- Serializable is the strongest isolation level that makes transactions behave as if they were executed one after another in sequence. This prevents all types of concurrency anomalies but comes with the trade-off of requiring retry logic in your application to handle transaction conflicts.
So, when should you use row-locking and when should you use a higher isolation level?
| Aspect | Serializable Isolation | Row-Level Locking |
|---|---|---|
| Concurrency | Lower - transactions might need to retry on conflict | Higher - only conflicts when touching same rows |
| Performance | More overhead - must track all read/write dependencies | Less overhead - only locks specific rows |
| Use Case | Complex transactions where it's hard to know what to lock | When you know exactly which rows need atomic updates |
| Complexity | Simple to implement but requires retry logic | More explicit in code but no retries needed |
| Error Handling | Must handle serialization failures | Must handle deadlock scenarios |
| Example | Complex financial calculations across multiple tables | Auction bidding, inventory updates |
| Memory Usage | Higher - tracks entire transaction history | Lower - only tracks locks |
| Scalability | Doesn't scale as well with concurrent transactions | Scales better when conflicts are rare |
When to Use PostgreSQL (and When Not To)
Let me revise that opening with more concrete technical advantages:
Here's my advice: in your system design interview, PostgreSQL should be your default choice unless you have a specific reason to use something else. Why? Because PostgreSQL:
- Provides strong ACID guarantees while still scaling effectively with replication and partitioning
- Handles both structured and unstructured data through JSONB support
- Includes built-in solutions for common needs like full-text search and geospatial queries
- Can scale reads effectively through replication
- Offers excellent tooling and a mature ecosystem
PostgreSQL shines when you need:
- Complex relationships between data
- Strong consistency guarantees
- Rich querying capabilities
- A mix of structured and unstructured data (JSONB)
- Built-in full-text search
- Geospatial queries
For example, it's perfect for:
- E-commerce platforms (inventory, orders, user data)
- Financial systems (transactions, accounts, audit logs)
- Content management systems (posts, comments, users)
- Analytics platforms (up to reasonable scale)
When to Consider Alternatives
That said, we aren't maxis over here. There are legitimate reasons to look beyond PostgreSQL.
1. Extreme Write Throughput
If you need to handle millions of writes per second, PostgreSQL will struggle because each write requires a WAL entry and index updates, creating I/O bottlenecks even with the fastest storage. Even with sharding, coordinating writes across many PostgreSQL nodes adds complexity and latency. In these cases, you might consider:
- NoSQL databases (like Cassandra) for event streaming
- Key-value stores (like Redis) for real-time counters
2. Global Multi-Region Requirements
When you need active-active deployment across regions (where multiple regions accept writes simultaneously), PostgreSQL faces fundamental limitations. Its single-primary architecture means one region must be designated as the primary writer, while others act as read replicas. Attempting true active-active deployment creates significant challenges around data consistency and conflict resolution, as PostgreSQL wasn't designed to handle simultaneous writes from multiple primaries. The synchronous replication needed across regions also introduces substantial latency, as changes must be confirmed by distant replicas before being committed. For these scenarios, consider:
- CockroachDB for global ACID compliance
- Cassandra for eventual consistency at global scale
- DynamoDB for managed global tables
3. Simple Key-Value Access Patterns
If your access patterns are truly key-value (meaning you're just storing and retrieving values by key without joins or complex queries), PostgreSQL is overkill. Its MVCC architecture, WAL logging, and complex query planner add overhead you don't need. In these cases, consider:
- Redis for in-memory performance
- DynamoDB for managed scalability
- Cassandra for write-heavy workloads
Summary
PostgreSQL should be your default choice in system design interviews unless specific requirements demand otherwise. Its combination of ACID compliance, rich feature set, and scalability options make it suitable for a wide range of use cases, from simple CRUD applications to complex transactional systems.
When discussing PostgreSQL in interviews, focus on analyzing concrete requirements around data consistency, query patterns, and scale, rather than following trends. Be prepared to discuss key trade-offs like ACID vs eventual consistency, read vs write scaling strategies, and indexing decisions. Start simple and add complexity only as needed.
PostgreSQL's rich feature set often eliminates the need for additional systems in your architecture. Its full-text search capabilities might replace Elasticsearch, JSONB support could eliminate the need for MongoDB, and PostGIS handles geospatial needs that might otherwise require specialized databases. Built-in replication often provides sufficient scaling capabilities for many use cases. However, it's equally important to recognize when PostgreSQL might not be the best fit, such as cases requiring extreme write scaling or global distribution, where databases like Cassandra or CockroachDB might be more appropriate.
Appendix: Basic SQL Concepts
Before diving into PostgreSQL-specific features, let's review how SQL databases organize data. These core concepts apply to any SQL database, not just PostgreSQL but are a necessary foundation that we will build on throughout this deep dive.
Relational Database Principles
At its core, PostgreSQL stores data in tables (also called relations). Think of a table like a spreadsheet with rows and columns. Each column has a specific data type (like text, numbers, or dates), and each row represents one complete record.
Let's look at a concrete example. Imagine we're designing a social media platform. We might have a users table that looks like this:
This command would create the following table (data is just for example):
| id | username | created_at | |
|---|---|---|---|
| 1 | johndoe | john@example.com | 2024-01-01 10:00:00 |
| 2 | janedoe | jane@example.com | 2024-01-01 10:05:00 |
| 3 | bobsmith | bob@example.com | 2024-01-01 10:10:00 |
When a new user signs up, we create a new row in this table. Each user gets a unique id (that's what PRIMARY KEY means), and we ensure no two users can have the same username or email (that's what UNIQUE does).
But users aren't much fun by themselves. They need to be able to post content. Here's where the "relational" part of relational databases comes in. We can create a posts table that's connected to our users:
| id | user_id | content | created_at |
|---|---|---|---|
| 1 | 1 | Hello, world! | 2024-01-01 10:00:00 |
| 2 | 1 | My first post | 2024-01-01 10:05:00 |
| 3 | 2 | Another post | 2024-01-01 10:10:00 |
See REFERENCES users(id)? That's called a foreign key - it creates a relationship between posts and users. Every post must belong to a valid user, and PostgreSQL will enforce this for us. This is one of the key strengths of relational databases: they help maintain data integrity by enforcing these relationships.
Now, what if we want users to be able to like posts? This introduces a many-to-many relationship - one user can like many posts, and one post can be liked by many users. We handle this with what's called a join table:
This structure, where we break data into separate tables and connect them through relationships, is called "normalization." It helps us:
- Avoid duplicating data (we don't store user information in every post)
- Maintain data integrity (if a user changes their username, it updates everywhere)
- Make our data model flexible (we can add new user attributes without touching posts)
Understanding these fundamentals - tables, relationships, and normalization - is important. They're what make SQL databases like PostgreSQL so powerful for applications that need to maintain complex relationships between different types of data. In your interview, being able to explain not just how to use these concepts, but when and why to use them (or break them), will set you apart.
ACID Properties
One of PostgreSQL's greatest strengths is its strict adherence to ACID (Atomicity, Consistency, Isolation, and Durability) properties. If you've used databases like MongoDB or Cassandra, you're familiar with eventual consistency or relaxed transaction guarantees which are common trade-offs in NoSQL databases. PostgreSQL takes a different approach – it ensures that your data always follows all defined rules and constraints (like foreign keys, unique constraints, and custom checks), and that all transactions complete fully or not at all, even if it means sacrificing some performance.
Let's break down what ACID means using a real-world example: transferring money between bank accounts.
Atomicity (All or Nothing)
Imagine you're transferring $100 from your savings to your checking account. This involves two operations:
- Deduct $100 from savings
- Add $100 to checking
Atomicity guarantees that either both operations succeed or neither does. If the system crashes after deducting from savings but before adding to checking, PostgreSQL will roll back the entire transaction. Your money never disappears into thin air.
Consistency (Data Integrity)
Consistency ensures that transactions can only bring the database from one valid state to another. For example, let's say we have a rule that account balances can't go negative:
If a transaction would make your balance negative, PostgreSQL will reject the entire transaction. This is different from NoSQL databases where you often have to enforce these rules in your application code.
Isolation (Concurrent Transactions)
Isolation levels determine how transactions can interact with data that's being modified by other concurrent transactions. PostgreSQL supports four isolation levels, each preventing different types of phenomena:
While the SQL standard defines four isolation levels, PostgreSQL implements only three distinct levels internally. Specifically, Read Uncommitted behaves identically to Read Committed in PostgreSQL. This design choice aligns with PostgreSQL's multiversion concurrency control (MVCC) architecture, which always provides snapshot isolation - making it impossible to read uncommitted data.
| Isolation Level | Dirty Read | Nonrepeatable Read | Phantom Read | Serialization Anomaly |
|---|---|---|---|---|
| Read uncommitted | Allowed, but not in PG | Possible | Possible | Possible |
| Read committed | Not possible | Possible | Possible | Possible |
| Repeatable read | Not possible | Not possible | Allowed, but not in PG | Possible |
| Serializable | Not possible | Not possible | Not possible | Not possible |
Durability (Permanent Storage)
Once PostgreSQL says a transaction is committed, that data is guaranteed to have been written to disk and sync'd, protecting against crashes or power failures. This is achieved through Write-Ahead Logging (WAL):
- Changes are first written to a log
- The log is flushed to disk
- Only then is the transaction considered committed
Why ACID Matters
In your interview, you'll often need to decide between different types of databases. ACID compliance is a crucial factor in this decision. Consider these scenarios:
- Financial transactions: You absolutely need ACID properties to prevent money from being lost or double-spent
- Social media likes: You might be okay with eventual consistency
- User authentication: You probably want ACID to prevent security issues
- Analytics data: You might prioritize performance over strict consistency
PostgreSQL's strict ACID compliance makes it an excellent choice for systems where data consistency is crucial. While performance trade-offs nowadays are minor, they're still worth being aware of.
SQL Language
Let's talk about SQL briefly. While you rarely write actual SQL queries in system design interviews, understanding SQL's capabilities helps you make better architectural decisions. Plus, if you're interviewing for a more junior role, you might be asked to write some basic queries to demonstrate database understanding.
SQL Command Types
SQL commands fall into four main categories:
- DDL (Data Definition Language)
- Creates and modifies database structure
- Examples: CREATE TABLE, ALTER TABLE, DROP TABLE
- DML (Data Manipulation Language)
- Manages data within tables
- Examples: SELECT, INSERT, UPDATE, DELETE
- DCL (Data Control Language)
- Controls access permissions
- Examples: GRANT, REVOKE
- TCL (Transaction Control Language)
- Manages transactions
- Examples: BEGIN, COMMIT, ROLLBACK
Test Your Knowledge
Take a quick 15 question quiz to test what you've learned.
Mark as read
The best mocks on the market.
Now up to 25% off
On This Page
A Motivating Example
Core Capabilities & Limitations
Read Performance
Write Performance
Replication
Data Consistency
When to Use PostgreSQL (and When Not To)
When to Consider Alternatives
Summary
Appendix: Basic SQL Concepts
Relational Database Principles
ACID Properties
Why ACID Matters
SQL Language
SQL Command Types
Schedule a mock interview
Meet with a FAANG senior+ engineer or manager and learn exactly what it takes to get the job.
MinorBlueDove142
You mentioned the same for DynamoDB as default choice in System Design - b/w Postgres and Dynamo, which one can I chose by default?
1
Evan King
I'll update the DDB article to ensure there is no confusion here. When in doubt, I'd lean towards Postgres. But the reality is, in many cases, there is no "wrong" answer here. It's about how you justify it.
In fact, "either works, but I X is what I have more experience with" is a reasonable justification, believe it or not. And it avoids the false dichotomy.
5
Evan King
I actually don't see where I say to choose DDB w/ regards to a default choice. Did you have the specific line? Would love to get that updated.
4
MinorBlueDove142
this is from DDB article: "For system design interviews in particular, it has just about everything you'd ever need from a database. It even supports transactions now! Which neutralizes one of the biggest criticisms of DynamoDB in the past."
I think I misunderstood it.
1
QuietBlackJay631
Funnily enough re: "either works, but X is what I have more experience with", in a recent interview round I got feedback that this was undesirable to say even though everything I said before it was justifications for using DDB instead of Postgres...so I'd caution that some interviewers only hear 'I want to use this because I know it' even if you've provided justifications.
7
MathematicalPurpleAmphibian147
Thanks for putting together this guide - outcome focused and effective. Quick Question - For interview Qs that dont need ACID, why would engineering complexity (developer skill/training, DB Admins, lower flexibility of schema changes, high monitoring effort) compared to a managed solution like DDB not a point of trade-off?
3
Evan King
Sounds like a totally valid trade-off to me!
What we're looking to avoid is statements like, "choose NoSQL because of scale," which show a lack of understanding of how far modern RDBMSs have come. But the list you have there are absolutely valid justifications.
2
Mike Choi
Hey Evan correct me if I'm wrong, but when discussing DB tradeoffs we should really be focused around the application needs and choosing a DB that supports that use case right?
For instance, you could say you will use Postgres for everything (since it can pretty much handle most application use cases), but then explain as the system scales, you want to add something specific, say for analytical data (and point to a wide column/columnar DB). Or you wouldn't want to choose Mongo right out of the gate just "because its flexible" when a standard RDBMS will often times lead to less headache down the road (for devs just learning about DB management).
About your point on scalability, modern day RDBMS can scale to extremely high volume of data and throughput, but the challenge probably then comes in how you optimize the database with indexes, replicas, partitioning, etc., right? Albeit, scaling out with a RDBMS is probably more difficult than a NoSQL DB.
These are on top of other points, i.e. CAP theorem, if your system even needs a distributed database system right away, emphasis on read or write throughput, ACID properties, etc...
2
Evan King
Yah, this is great. The key is just to show your interviewer that you understand nuance. This does not need to be a lot of information, but avoid the false SQL vs NoSQL dichotomy. Something as simple as, "Our data has XYZ property, so I'll opt for W because it handles this well and is what I know best," is a great answer.
0
ExtraAmaranthDuck389
That's awesome!
0
PersonalBrownFalcon225
How do we support atomic transactions if we have multiple nodes (I think we need something like 2 phase commit here?)
3
BeneficialBrownBarnacle904
Is there a recommendation on when a dedicated search optimized database like ElasticSearch would be needed vs when Postgres GIN/ PostGIS indexes would be sufficient?
1
Evan King
Added a section for this! Good question. If you need:
Main one being if you're growing into the 100Ms of rows, postgres might not be the best fit for search.
1
BeneficialBrownBarnacle904
Thank you for the detailed answer and the bonus section! :D
0
RainyCyanHippopotamus256
"Main one being if you're growing into the 100Ms of rows, postgres might not be the best fit for search."
Why postgres might not be the best fit for many rows? Is it because ES can shard the data and horizontally scale the data automatically with ability to search through them by aggregating the data? But with postgres, it is generally a read-replica which could be a bottleneck?
1
Evan King
PostgreSQL could technically can handle large-scale search through manual sharding, but you'd need to build your own logic to merge and rank results across shards which could be a pain. But, Elasticsearch was purpose-built for this. Automatic sharding and cross-node result aggregation are built into its core architecture.
1
RainyCyanHippopotamus256
Thanks. Yup, that's what I meant.
0
Eneias Silva
Can you elaborate on Real-time index updates? How is the Postgres index updated? Isn’t it in real-time? Also Elastic Search index must be kept in sync with the main DB
0
Jiatang Dong
"PostgreSQL supports two main types of replication: synchronous and asynchronous. Synchronous replication ensures that changes are written to disk on the primary node before being acknowledged, while asynchronous replication allows writes to complete on the primary node before being acknowledged."
I find this paragraph is confusing, can you explain more?
0
cst labs
this is a typical replication setup. Imagine a write to the leader in synchronous replication. The response to the write is blocked until the replica doesn't acknowledge or the request times out. This leads to high consistency since every write is acknowledged. However, it leads to bad experience and affects availability. Async replication is actually async where the leader won't block for an ack from the replica. Though, an organization may choose to have a limited number of synchronous and rest async replication. A consensus algorithm is used for this purpose with a defined quorum.
0
RadicalBlackBeetle554
I think, there must be some typo. Evan, could you please take a re-look in that paragraph (synchronous vs asynchronous replication) and clarify.
0