accesspatterns.dev
← All models

compliance

Change & Audit Log

An append-only history beside the current state it explains.

Every change to a resource is written as an immutable event, and the resource's current state lives in the same partition with a version number. One Query returns the state and the history that produced it.

The model

CurrentState

What the resource looks like now, and which version that is.

pk
RESOURCE#<resourceId>
sk
CURRENT

Attributes: status (S), version (N), updatedAt (N)

ChangeEvent

One immutable record of a change, numbered in sequence.

pk
RESOURCE#<resourceId>
sk
EVENT#<sequence>

Attributes: action (S), actor (S), at (N)

Access patterns

  • QueryRead a resource and its history

    The current state and every event that led to it.

  • UpdateItemApply a change

    Update the current state, only if nobody else has since you read it.

  • GetItemRead the current state

    Just the resource as it stands, without its history.

Design notes

CURRENT sorts before every EVENTwhy

The sort key labels put the current state at the head of the partition, ahead of the log. So a Query that stops after one row is a cheap read of the present, while the same Query run out returns the whole history. One key design, two very different reads.

The version number is the lockwhy

An update carries the version it read and asks DynamoDB to apply the change only if version still matches. Two reviewers acting on version 3 both send the same condition; the first wins and moves it to 4, the second is refused and has to re-read. No row is locked and nothing waits - the loser simply finds out.

The state and its event are one write, or neithertrade-off

Advancing CURRENT and appending its EVENT# are two writes, and a crash between them leaves a resource whose state no event explains - which is the one thing an audit trail exists to prevent. Production binds them in a single TransactWriteItems: the guarded update and the conditional append succeed together or not at all. The access pattern here shows only the guarded update, because that is the half this format can express.

Applying a change is a design here, not a live demonstrationtrade-off

The preview console cannot run the guarded update: the resolver templates writes but does not emit them as runnable, so there is no button here that races two reviewers. The engine itself does support the mechanism, so this is a limit of what the page can show rather than of what DynamoDB does.

Events are numbered, not timestamped, for orderingwhy

at records when a change happened, but EVENT#000001 is what orders the log. Two changes in the same millisecond collide on a timestamp key and one silently overwrites the other; a sequence removes that. It does not remove contention on its own - two writers can still reach for the same number - so the number comes from the version the guarded update just bumped, and the event is written with attribute_not_exists on its sort key so a second claimant is refused rather than overwriting. The padding matters as much as the number, since sort keys compare as strings and EVENT#10 sorts before EVENT#2 without it.

Taught in the course

audit-log
Try an example

The current state and every event that led to it.

PK(pk)
SK(sk)
status
updatedAt
version
action
actor
at
RESOURCE#policy-4471
CURRENTS
approvedS
1786579200N
3N
RESOURCE#policy-4471
EVENT#000001S
createdS
adaS
1783209600N
RESOURCE#policy-4471
EVENT#000002S
submittedS
rajS
1785888000N
RESOURCE#policy-4471
EVENT#000003S
approvedS
meiS
1786579200N
RESOURCE#policy-9930
CURRENTS
draftS
1785628800N
1N
RESOURCE#policy-9930
EVENT#000001S
createdS
samS
1785628800N

Run an operation to see the raw engine response.

table audit-logkeys PK / SKitems 6

The in-browser engine is a preview build. Transactions, vector search, streams, tags and TTL are among the operations it doesn't implement yet.