Reladomo → DynamoDB

The bitemporal seam

How a Java ORM built for relational databases persists to DynamoDB without reimplementing a single rule about time.


One box changes

Reladomo is a bitemporal ORM: every row knows both when a fact was true and when the system learned it. The instinct when porting that to DynamoDB is to rebuild those rules on top of a key-value store. That instinct is wrong, and the reason is structural.

Reladomo's temporal logic lives in its TemporalDirector, which sits above persistence. By the time anything reaches the storage layer, the director has already decided which rows should exist and handed down plain inserts, updates and deletes — each carrying its own timestamps.

IDENTICAL ABOVE THE SEAM Your finder calls MithraObjectPortal TemporalDirector terminate · updateUntil · incrementUntil plain insert / update / delete RELATIONAL JDBC persister SQL database RELADYNAMO DynamoDbPersister DynamoDB
Everything above the fork is unchanged Reladomo — the same object model XML, the same generated finders, the same call sites. Reladynamo substitutes one implementation and the store behind it.

The practical consequence: bitemporality is inherited, not rewritten. Any temporal reasoning added below that seam would drift from a relational Reladomo — which is precisely what the differential test suite exists to catch.


Two axes, not one

Most developers meet "bitemporal" as a word before they meet the problem. The problem is ordinary: you learn things late.

A balance was 10 from January. In June you discover it should have been 99 from March. A single-axis store must choose: overwrite history and lose what you reported in April, or keep the old value and lie about the present. Bitemporal storage refuses the choice by recording both — when the fact was true and when you knew it.

business time — when the fact was true processing time — when we knew Jan Mar ∞ Jan Jun ∞ quantity = 10 what we believed Jan–Jun 10 still true quantity = 99 the correction correction recorded in June
One setQuantity(99) against a March business date. The director closes the old belief at June and opens two replacements — so the April report still reproduces exactly, and today's query returns 99.
what we believed then what we believe now the moment we learned

Where the rectangles go

DynamoDB has no notion of a rectangle. Each corner of that picture becomes one item, addressed by a partition key and a sort key. The sort key is where both axes live:

v1 P#20260601000000000 processing-from B#20260301000000000 business-from SORT KEY a prefix — native range query a suffix — cannot be a range condition Fixed-width, UTC, zero-padded — so byte order equals chronological order, including before 1970, where a signed epoch offset would sort backwards.
Processing-major ordering is deliberate. It makes "every version of this row, newest knowledge first" a single query — at the cost that a business-date-only predicate becomes a filter rather than a range scan. The planner does that openly instead of pretending otherwise.

Why the rules are the interesting part

The clearest demonstration isn't a ledger — it's a decision table where the rules themselves are bitemporal. Classify cars by year, make and body style. What counted as cool in 2012 counts as retro now, and the car never changed:

Car 2012 2016 2021 today
1985 Toyota MR2 coupe COOLEIGHTIES_COOLRETRORETRO
1957 Chevrolet Bel Air coupe CLASSICCLASSICCLASSICVINTAGE
2018 Tesla Model 3 sedan ——MODERNMODERN

That table is real output from a runnable demo, not an illustration. Every column is the same query against the same unchanged rows, asked as of a different date.

Then the second axis earns its keep: correct a rule that was wrong when it was written, re-run a 2012 classification, and it disagrees with the audit row stored in 2012. What we said then versus what we now think we should have said then — two different questions, both answerable, which is the entire reason this machinery exists.


What it costs

Stated plainly, because discovering these in production is worse than reading them here. The full list is the support contract.

TransactionsDynamoDB has no interactive transaction. A Reladomo transaction commits as one TransactWriteItems across all entity tables, which caps at 100 actions and 4 MB; wider scopes are refused rather than chunked.
Arbitrary predicatesQueries start from an exact partition key. Anything else needs a Scan, which is opt-in — the planner refuses by default rather than running one quietly.
Item size400 KB per item; 2048-byte partition key; 1024-byte sort key.
DecimalsStored as strings. DynamoDB's numeric type trims trailing zeros, which would silently change the scale of money.