Sharding
For datasets too large for one database, ShardRouter partitions data across
several shard databases by a shard key (e.g. user_id, region). Each
row lives on exactly one shard; a query routes to the shard that owns its key.
Unlike read replicas, shards hold disjoint data and
scale writes as well as reads.
ShardRouter is a ClientProvider, so it drops into quark.For[T] like any
client — the rest of the ORM is unaware of sharding.
shards := map[string]*quark.Client{
"shard-a": clientA, // each is a normal quark.New(...) client
"shard-b": clientB,
}
router, err := quark.NewShardRouter(
shards,
quark.DefaultShardResolver, // reads the key set by WithShardKey
quark.HashShardFunc([]string{"shard-a", "shard-b"}), // FNV-1a hash → shard
)
// The caller supplies the shard key (a string) per operation, via the context.
shardCtx := quark.WithShardKey(ctx, user.Region) // shard by region
_ = quark.For[User](shardCtx, router).Create(&user) // → the shard owning user.Region
got, _ := quark.For[User](shardCtx, router).Where("id", "=", user.ID).List()
A complete, self-contained runnable example (two SQLite shards, no Docker) lives
in examples/sharding/ —
run it with go run ./examples/sharding/main.go. It routes accounts by shard
key, proves the data is disjoint per shard, and shows the keyless-query
rejection.
How routing works
DefaultShardResolverreads the shard key from the context (WithShardKey);ShardKeyFromContextexposes that value if you need it. You can supply your ownShardResolverto read an existing request value.- The
ShardFuncmaps that key to a shard name.HashShardFuncis the stable hash-mod default; supply your own for range, geo, or lookup-table policies — it is the seam you control for resharding. - The query runs on that shard's
*Client, unchanged.
A query without a shard key in context errors — there is no implicit cross-shard fan-out (forgetting the key fails loudly rather than silently querying every shard).
Limits
- No cross-shard joins — a
JOINonly sees the resolved shard. - No cross-shard transactions — a
Txis bound to one shard's client; there is no two-phase commit. Design the model so each operation stays within a shard (choose the shard key well; denormalize where needed). - Scatter-gather reads (query all shards and merge) are not yet available — a deliberate follow-up, not an implicit fallback.
- Resharding (changing the
ShardFunc+ migrating data) is an operator task; the shard set is fixed at construction.
Multi-tenancy composes orthogonally: a shard's *Client can itself sit behind
a TenantRouter. See
ADR-0016
for the full rationale.