A FastAPI + Postgres sandbox that demonstrates database sharding: global database, consistent hashing, colocation, cross-shard fan-out reads, and online resharding via dual-write.
A did Deep dive and walkthrough of the various concepts implemented here, Medium article. I would recommend reading that first if you're new to database sharding, so the code is more understandable.
docker compose up -d
docker compose exec backend python scripts/migrate.py upgrade head
docker compose exec backend python scripts/seed.pyThe API is at http://localhost:8000. pgweb tabs per DB:
| DB | Port |
|---|---|
| shard_0 | 8081 |
| shard_1 | 8082 |
| shard_2 | 8083 |
| global | 8084 |
| shard_3 | 8085 (migration profile only) |
ring.py—ConsistentHashRingwith virtual nodes. MD5 hash +bisectfor shard lookup. Run directly (python sharding/ring.py) for a distribution + resharding demo.router.py—ShardRouter. Holds engines for every shard + global, owns the ring(s), exposesshard_for(key, "READ"|"WRITE"),session(name),all_shards(). ReadsCONFIGS["DB_RESHARDING"]at init time and wires up a second_new_ringwhen phase isDUAL_WRITE.
Module-level ShardRouter singleton + get_session_shard(name) facade for FastAPI handlers.
user.py— sharded byid. Email isuniqueper shard; cluster-wide uniqueness is enforced via Redis SETNX.review.py— sharded byuser_id(colocates withUser). FK touser.idenforced at the shard level. No FK toproduct(different DB).product.py— lives on the global DB, unsharded.
user.py—POST /usersclaims email in Redis, then writes the user to every shard returned byshard_for(..., "WRITE")(one shard normally, two during DUAL_WRITE for moving keys). Releases the claim on any DB error.review.py—POST /reviewsvalidates the product on global, then writes to the WRITE shard list.GET /products/{id}/reviewsfans out toall_shards()in parallel viaThreadPoolExecutor, does a localUser+ReviewJOIN per shard, then merge-sorts on(created_at, id)with cursor pagination.product.py— straightforward CRUD on the global DB.
Redis client used for cross-shard email uniqueness (SETNX claim + lazy reconciliation).
migrate.py— parallel alembic runner. Drift check before + after, status board, non-zero exit on divergence. Each DB runsalembicin a child process withDATABASE_URLoverridden.seed.py— deterministic seed (50 products, 100 users, 250 reviews). Builds its own ring to assign users → shards before insert.resharding_backfill.py—ADDflow, runs duringDUAL_WRITE. Keyset-paginates every old shard and copies the rows whose key now routes to the new shard. Idempotent viaON CONFLICT DO NOTHING.resharding_cutover.py—ADDflow, runs duringCUTOVER. Scans every shard and deletes rows whose key no longer routes there. Reviews first, then users (FK order). Idempotent.resharding_decommission.py—REMOVEflow, runs duringDUAL_WRITE. Drains the doomed shard, routing each row to its new home via_new_ring. Mirror of backfill (one source → many destinations). Idempotent.resharding_wipe.py—REMOVEflow, runs duringCUTOVER. Runsalembic downgrade baseon the doomed shard to drop all its tables, leaving it empty and ready to tear down.
Single revision history shared by every DB. env.py reads DATABASE_URL from the env (set by migrate.py) and falls back to the global DB URL for autogen.
Environment-keyed configs. SHARD_URLS is the live ring topology; DB_RESHARDING is the migration control block (OPERATION, TARGET_SHARD_NAME, TARGET_SHARD_URL, DB_RESHARDING_PHASE).
Driven by CONFIGS["DB_RESHARDING"]: OPERATION (ADD | REMOVE), TARGET_SHARD_NAME, TARGET_SHARD_URL, and DB_RESHARDING_PHASE. During DUAL_WRITE, reads stay on the old ring; writes fan out to both rings when the key is moving. During CUTOVER, the ring is updated and reads/writes flow normally.
Walks shard_3 into the cluster.
IDLE -> ALLOW_MIGRATIONS -> DUAL_WRITE -> CUTOVER -> IDLE (shard_3 promoted)
- Start
shard_3—docker compose --profile migration up -d - Set
OPERATION=ADD,TARGET_SHARD_NAME=shard_3,MIGRATION_PHASE=ALLOW_MIGRATIONSin.envorconfigs/config.py python scripts/migrate.py upgrade head— bringsshard_3's schema to parity- Set
MIGRATION_PHASE=DUAL_WRITE python scripts/resharding_backfill.py— copies historical rows toshard_3- Set
MIGRATION_PHASE=CUTOVER, restart backend python scripts/resharding_cutover.py— deletes stale rows on old shards- Move
shard_3intoSHARD_URLS, setMIGRATION_PHASE=IDLE, restart
Walks shard_3 back out. No ALLOW_MIGRATIONS step (existing shards already have the schema).
IDLE -> DUAL_WRITE -> CUTOVER -> IDLE (shard_3 gone)
- Set
OPERATION=REMOVE,TARGET_SHARD_NAME=shard_3,MIGRATION_PHASE=DUAL_WRITE, restart backend python scripts/resharding_decommission.py— drainsshard_3across the remaining shards- Set
MIGRATION_PHASE=CUTOVER, (now the router pretendsshard_3doesn't exist) python scripts/resharding_wipe.py— drops every table onshard_3viaalembic downgrade basedocker compose stop shard_3— tear the container down- Remove
shard_3fromSHARD_URLS, setMIGRATION_PHASE=IDLE, restart
# Inside the backend container
alembic revision --autogenerate -m "your migration"
python sharding/ring.py # ring distribution + resharding demo
python scripts/migrate.py upgrade head
python scripts/migrate.py upgrade head --timeout 600