THE MIGRATION LINE
INCIDENT // DOORDASH-2023 MIGRATED

DoorDash's push into grocery and convenience multiplied menu items until a single-writer Postgres table hit its internal 500GB ceiling. They built a facade layer first, then swapped the store underneath it for CockroachDB.

DOORDASH · 2023 · DATABASE / COCKROACHDB / POSTGRESQL / DISTRIBUTED-SQL
System stress over time Breach at T+3
500GB Internal PG table limit
8 → 2 Secondary indices
10x faster Composite-key queries
5x faster Update performance
BASELINE

A materialized view outgrowing its single writer

store_items was a materialized view living in PostgreSQL, holding the catalog, inventory, and pricing data for every convenience and grocery item on DoorDash, and serving that data to Dashers during order fulfillment. When DoorDash expanded from restaurant delivery into new verticals like convenience and grocery, the number of integrated merchants grew dramatically — and so did the number of menu items, since grocery and convenience stores carry far larger, more complicated inventories than a typical restaurant. That volume showed up first as high latency and a high failure rate in the fulfillment backend.

The database told the same story in numbers. OLTP usage climbed to 500GB very quickly, past DoorDash’s own internal guideline that a single PostgreSQL table stay under that size. Because every update funneled through one writer instance, SQL inserts slowed down; at peak hours, large non-batched, non-partitioned upserts doubled overall service latency and pushed database CPU usage past 80%.

REQUIREMENTS

Scale for 10x, and stop paying for old habits

The legacy store_items table had gotten the new-verticals business off the ground, but the team needed it to comfortably support 10x current volumes — not a patch on today’s numbers. That meant solving three problems at once rather than one at a time: raw scale (the 500GB ceiling), operational fragility (a single-AZ writer as a single point of failure), and a backlog of anti-patterns baked into years of legacy queries.

THE OPTIONS

Split the table, offload to S3, or replace the database

Three paths were actively explored. Split store_items into multiple tables so each one stayed under 500GB. Move the bulk data into S3 blob storage and keep only the URLs in the database table. Or adopt CockroachDB outright, solving the scalability problem “once for all” while keeping a structured, tabular SQL data model. The team chose CockroachDB, citing its shared-nothing architecture — independent nodes with their own local storage that can be added or removed without disrupting the system — along with native support for distributed transactions and changefeeds for building event-driven consumers downstream.

THE CALL

A facade first, then a four-milestone migration underneath it

Before touching the database, the team cut off all direct database access behind a new service facade called Retail Fulfillment Data Service (RFDS), exposing three or four predominant query patterns as gRPC APIs. Because callers only ever talked to RFDS, the schema underneath it could change without any client noticing — which is exactly what happened next, across four milestones: migrate every legacy data-retrieval flow onto the new facade; make schema changes and backfill the store (splitting frequently-updated columns like price and availability into their own column families for 5x faster updates, grouping data into JSONB columns, dropping SQL joins in favor of downstream service calls joined in memory, and cutting secondary indices from eight down to two by relying on a composite primary key); run shadow reads and data comparisons; then cut over and clean up.

CockroachDB started as an async shadow, populated on every real-time write and bulk-import job while the legacy database stayed primary. The team compared both stores on existence and attribute equivalence using a “Mapdifference” API to catch skew, and once reads were consistent, flipped the roles — CockroachDB became primary, the legacy database became the fallback shadow — and repeated the comparison, this time including key business metrics.

CONSEQUENCES

Faster composite-key queries, and a schema gamble that paid off

Once traffic was gradually rolled over through a feature flag, the results split by query pattern. Bulk queries (10k+ rows) by store_id dropped roughly 38% in latency, because store_id is the first column of CockroachDB’s composite primary key versus just an ordinary secondary index in Postgres. Queries against dd_menu_item_ids, a secondary index in both systems, came out on par. Queries against store_id plus merchant_supplied_id — a true composite key in CockroachDB, only a composite secondary index in Postgres — came out 10x faster. New-verticals fulfillment can now handle 10x the load it was built for.

Put a facade in front of your database before you migrate it, and the migration underneath can take risks — schema redesign included — that a direct-access system never could.

Source — read the original

https://careersatdoordash.com/blog/how-we-scaled-new-verticals-fulfillment-backend-with-cockroachdb/

A plain-language, AI-drafted and human-edited retelling of the article published on careersatdoordash.com, reorganized and explained in our own structure and words, with original analysis in the editor's note above. The facts, numbers, and decisions belong to the original author and are not altered. For the full depth, read the source.

← All systems