Table of Contents

Design patterns

DynamoDB has no joins and few indexes, and it finds an item by its key and nothing else. So a table is designed from the questions it must answer, not from the shape of the data: list the access patterns first, such as "the orders of a customer, newest first" or "the open tickets of an agent, oldest first", and give each one a key or an index that answers it with one Query. Single-table design follows from that. Items that are read together share a table and a partition, whatever their type, so one request reads them, and the discriminator _type tells them apart.

DynaSharp was designed around a catalog of such patterns. Each one runs as a scenario test, in the recipes, and the examples on these pages come from those tests. They share one table: shop is client.Table("Shop", "pk", "sk"), and shop.Of<T>() opens it for one type. Tables says how to create it.

Pattern What it is for
Composite sort keys A hierarchy in one partition, read at any depth with begins_with
Newest first with version-7 GUID sort keys Sort keys that are unique and in time order, read newest first
Write sharding A key that takes more writes than one partition can, spread over several
Time-bucketed partitions A stream of events that must not grow one partition without end
Large objects A value over the item limit of 400 KB, in chunks
Adjacency list and inverted index Several types in one partition, and a relation walked both ways
Index overloading One global index that answers a question per type
Sparse index The few items in a rare state, read at the cost of those
Status-keyed index "My open items, oldest first"
Queue over a sparse index Jobs that workers claim one at a time, with a reaper for a worker that stopped
Soft delete Items that leave the lists when deleted, and can come back
Optimistic locking A read-modify-write that loses no update, without a lock
Uniqueness constraints A value other than the key that must be unique
Idempotency keys A request that a client retries, and that takes effect once
Counters Counting on the server, and a counter that a retry cannot count twice
Hi/lo sequence Numeric ids without a write per id
Transactional outbox A change of state and the event about it, both or neither

More recipes

These run as tests too, with the same primitives, and each file explains its pattern at the top.

  • Vertical partitioning: what changes often goes into an item of its own in the same partition, so a write pays for that item alone.
  • Version-0 history: the current state at v0 and an immutable item per change, written in one transaction.
  • Bitemporal sort key: when a fact holds and when it was recorded, both in the key, so a correction overwrites nothing.
  • Sharded counter: a counter hotter than one item, spread over shards and summed by a fan-out.
  • Sharded "latest across everything": the newest n of the whole table, from a global index over a few shards.
  • Gapless sequence: numbers without holes, as invoices need, in the transaction of the record that takes one.
  • Table per period: a table per month, dropped as a whole when its data expires.
  • Inverted index: an item per term and document, for search at trivial scale.
  • Autocomplete: suggestions under a folded sort key, read with begins_with on what was typed.
  • Top-N / leaderboard: scores in order in a sharded global index, with a fan-out for the top.
  • Geohash proximity: places in the partition of their grid cell, and "near here" as nine cells.
  • Delayed jobs: jobs in the partition of the hour they are due, claimed with a conditional delete.
  • Pub/sub via subscription items: a copy of each message in an inbox per subscriber.
  • Fixed-window rate limit: a counter per caller and minute, raised with a condition on the limit.
  • Token bucket: a rate with bursts, refilled on the client under a condition on what it read.
  • Reservation with auto-release: a hold that ends by its time, with nothing that has to release it.
  • Service registry with liveness: instances that renew a time, and readers that filter on it.
  • Counting semaphore: at most n holders across processes, an item per permit with a lease. It has no fencing token, so a holder whose lease ran out can still act while the next one holds the permit: it bounds concurrency only among holders that stop at the end of their lease.
  • Barrier: n parties add their names to a set, and exactly one sees it complete.
  • HyperLogLog: distinct counts in a fixed kilobyte, merged on the client under [Version].
  • Documents of an external contract: an event whose schema another team owns, kept as a native map in a JsonElement property, filtered, projected and updated by paths inside it, see JSON of a contract.

Locks and leader election

A distributed lock needs more than a conditional write: a heartbeat, a wait on a monotonic clock, fencing tokens. The package DynaSharp.Coordination has a lease on a named lock with fencing tokens, and leader election on top of it, see Coordination. The counting semaphore above is the lighter form without fencing.

Not in DynaSharp

Rollups through DynamoDB Streams need a consumer that keeps its position and shares the shards among processes, such as a Lambda function. ReadChangesAsync reads the changes in one process, and is no such consumer.