The problem and the scale
In 2020 Figma ran its metadata (permissions, file info, comments) on a single Amazon RDS Postgres database, and database traffic was growing roughly 3x per year. Peak CPU was above 65%, latency became less predictable near that limit, and the team had already moved from an r5.12xlarge to an r5.24xlarge, the largest instance available at the time. Their own blog states the stakes plainly: if the database saturated, Figma would stop working.
Between 2020 and 2024 the database stack grew almost 100x. The first answer was vertical partitioning: move groups of related tables, such as "Figma files" or "Organizations", into their own databases. The last of those operations, in October 2022, moved 50 tables at once. Each operation cost roughly 30 seconds of partial impact, about 2% of requests dropped, and afterwards the largest partition sat near 10% CPU.
Vertical partitioning has a hard floor: the smallest unit you can move is one table. By the end of 2022 Figma had caching, read replicas and a dozen vertically partitioned databases, and some individual tables had reached several terabytes and billions of rows. Those tables hurt reliability during Postgres vacuums, and the highest-write ones were approaching the IOPS limits of RDS. The next step had to split a single table across machines.
What they refused to do
The interesting part of this case is the list of options Figma rejected, because it explains every mechanism that follows. Distributed SQL systems (CockroachDB, TiDB, Spanner, Vitess) meant a cross-store migration under pressure and throwing away years of in-house RDS expertise, with only months of runway. NoSQL meant rewriting most of a backend built on a relational model. A fully generic sharded database built in-house was out of scope for a small team.
So they built something narrow on purpose. The goals were explicit: minimal impact on application developers, scale out later without further application changes, no backfills that take months, incremental shipping with rollback possible even after physical sharding, strong consistency and near-zero downtime. The non-goals were just as explicit: no atomic cross-shard transactions, and only the subset of Postgres that most product queries actually use.
That combination of constraints produced three mechanisms.
Mechanism 1: colos, a unit of co-location chosen by shard key
Figma shards on a small set of keys, mainly UserID, FileID and OrgID, and almost every table could be attached to one of them. Tables that share a sharding key and a physical layout form a "colo". Inside a colo, joins and full transactions keep working as long as the query is filtered to a single sharding key value, because all those rows live on the same shard.
This is the Aggregate rule from Domain-Driven Design, enforced by storage. A transaction stays inside the boundary of one file or one organization, and the database layout guarantees it. Foreign keys are only supported when the foreign key is the sharding key, and global unique indexes only when they include the sharding key. Anything else becomes the application's problem.
Routing uses a hash of the sharding key rather than its raw value. Auto-incrementing and Snowflake-style IDs are time-ordered, so range-based routing would send all new writes to the same shard. Hashing spreads the load, and the price is that range scans on the sharding key become expensive. Figma accepted that because such scans are rare in its codebase, which is a judgment you have to verify on your own query log before copying it.
To choose keys without guessing, the team built a "shadow application readiness" framework that predicted how live traffic would behave under candidate sharding keys and told product teams which call sites to refactor. Back in the vertical partitioning era, they had already logged production queries and transactions to Snowflake to find tables consistently queried together. The co-location decision is made from traffic data, not from an entity diagram.
What it costs: every product team must thread a sharding key through its queries, and features that genuinely span users or organizations lose transactions and joins across that boundary.
Mechanism 2: logical sharding with Postgres views, before any data moves
This is the decision that makes the whole project survivable. Figma separated two operations that most sharding projects do at once.
Logical sharding changes how the application sees and routes data. Each logical shard is a Postgres view over the existing, still unsharded table, filtered to a hash range of the shard key. Each view gets its own sharded connection pooler, which still points at the same unsharded instance. Traffic moves onto the shard views through feature flags, by percentage, and rolling back is a configuration change that takes seconds. At this stage every query already behaves as if the data were sharded, but every byte is still on one machine.
An illustrative version of the idea (not Figma's code) looks like this:
-- Logical shard 0 of 4 over an unsharded table.
-- The application now queries files_shard_0 instead of files.
CREATE VIEW files_shard_0 AS
SELECT *
FROM files
WHERE hash_shard_key(file_id) >= 0
AND hash_shard_key(file_id) < 16384;
Figma validated the views against a sanitized production query corpus and a shadow reads framework. The overhead of querying through views was minimal in most cases and under 10% in the worst cases.
Physical sharding comes second: copy the data from one database to N databases and reroute traffic. Figma reused much of its vertical partitioning failover tooling for this, with a new failure mode to handle, partial success where only some of the N target databases complete. Their replication choice is unusual: full logical replication, where each shard receives the full dataset but serves only its own subset, instead of filtered replication.
A topology library maps tables to shard keys and logical shard IDs to physical databases. It pushes updates to clients in under a second and enforces invariants such as "each logical shard ID maps to exactly one physical database". Because the logical layout is independent of the physical one, non-production environments run the production logical topology on far fewer physical databases.
The result: work started in late 2022, the first sharded table, a simple high-traffic one, shipped in September 2023 after roughly nine months, and the physical failover caused about ten seconds of partial availability on primaries, no impact on replicas, and no latency or availability regression afterwards.
What it costs: you pay view overhead in production for the whole logical phase, you carry two layers of routing configuration, and rollback after physical sharding still needs real coordination, unlike rollback of the logical step.
Mechanism 3: DBProxy, a deliberately limited query engine
The routing decisions have to live somewhere. Figma put them in DBProxy, a Go service between the application and PgBouncer. Besides load-shedding, request hedging, observability, transaction handling and topology management, it contains a small query engine in three stages: a parser that turns SQL into an AST, a logical planner that extracts the query type and the logical shard IDs, and a physical planner that maps logical shards to physical databases and rewrites the query.
A query that carries a sharding key goes to a single shard, and execution is pushed down to Postgres. A query without one triggers scatter-gather: DBProxy fans it out to every shard and merges results. Each scatter-gather adds, on every database, the load of an unsharded query, so a popular query without a shard key cancels the benefit of sharding.
Figma did not try to support all of SQL. A shadow planning framework logged the plans of live traffic to Snowflake, and the team picked a query language that covers about 90% of common queries while avoiding worst-case complexity. Point queries and range scans are supported. Joins are only allowed between tables of the same colo and on the sharding key. Everything else must be rewritten by the owning team.
What it costs: a new critical hop on every database request, a service your team must own, and a contract with product engineers ("this SQL is no longer allowed") that needs tooling and patience to enforce.
What this changes compared to the principles you already apply
If you practice DDD, colos are aggregates promoted to a storage contract: the consistency boundary of the domain becomes the physical boundary of the data. If you practice event-driven architecture or CQRS, you would usually solve cross-aggregate reads with projections. Figma instead keeps a synchronous scatter-gather path, limited by policy, because rewriting the product around projections was exactly the large application change they wanted to avoid.
The more transferable idea is the decoupling of logical and physical topology. It is the same move as the strangler fig pattern, applied to data: introduce the new interface (shard views and DBProxy) while the old implementation (one database) still sits behind it, prove it with production traffic, then swap the implementation. Figma's earlier vertical partitioning used the same trick at the pooler level, by splitting PgBouncer into two services that pointed at the same database before moving any data, so routing mistakes could be detected without failing.
Figma is also honest about what remains open: sharded schema changes, globally unique ID generation for sharded primary keys, atomic cross-shard transactions for a few critical cases, distributed unique indexes, an ORM aware of sharding, and one-click resharding. They also state they will re-evaluate in-house sharding against NewSQL or managed options once runway is secured. In-house was a decision made under time pressure, not a doctrine.
What to steal for your architecture
Adopt this approach when you are on Postgres, a single table is the bottleneck (size, vacuum, IOPS), your access patterns are dominated by one of a few natural keys such as tenant, user or document, and your team knows Postgres operations better than any distributed database. Do not adopt it when your hot queries cross tenants by nature (marketplaces, global search, analytics), when you need cross-entity transactions everywhere, or when a managed distributed SQL service would fit your timeline: building DBProxy is a multi-year commitment.
For a single team, do not build a proxy. Make sure every table carries a tenant or owner key now, ban queries that cannot be scoped by it in code review, and use native Postgres partitioning by hash of that key on the biggest table. That prepares the logical model at almost zero cost.
For ten teams, introduce the logical layer before you need the physical one: a routing library or a thin data access service that resolves "key to shard", a topology table in configuration, and a query log analysis that lists every call site without a shard key. Vertical partitioning of independent table groups, behind separate poolers, is the cheapest next step, exactly as it was for Figma.
For a hundred teams, you are where Figma was. Treat the query engine as a product with an explicit supported SQL subset, put shadow reads and shadow planning in place before any cutover, separate logical and physical moves, and plan each complex table group (dozens of tables, thousands of call sites in Figma's case) as its own migration. Re-evaluate the build decision every year against managed alternatives, as Figma itself plans to do.

