Reladynamo · the price, stated

What you give up

Your object model and your finder calls come with you to DynamoDB. Some of what the relational database did for you does not. Here is that list, with the workaround for each item, or the word none.


At a glance

Each item is one of three kinds. Permanent is a property of DynamoDB, so no amount of adapter work removes it. Not yet is a gap in the adapter or its evidence. Out of scope is work that 0.1.0 has declared it does not do.

What you loseKindWorkaround
Units of work over 100 items or 4 MBPermanentSplit into units that are each correct on their own
Reading your own uncommitted writes from the storePermanentRead before the first write, or use objects already in the cache
Queries on any predicatePermanentSupply the key, add a GSI, or opt in to Scan
Items over 400 KB, oversized keysPermanentMove large values out of the row
Read-your-writes through an indexPermanentRead by full key instead. Index reads: none
Unique constraints off the primary keyPermanentModel the unique value as its own keyed entity
15 persister features: aggregates, cursors, mass delete…Not yetNone. Rewrite the call site
Online migration, the way back, ASEOut of scopeOffline copy with a write freeze
Evidence from real AWS and real loadNot yetRun your own preflight

Permanent

A transaction has a hard edge

With a relational database, a Reladomo unit of work gets an open transaction. It can write, read back what it wrote, and commit any number of rows. DynamoDB has no open transaction. The adapter holds every write in memory and sends them all at commit as one TransactWriteItems request covering every entity table. DynamoDB caps that request at 100 actions and 4 MB.

STAGED IN MEMORY UNTIL COMMIT 101 100 slots: one action per item filled: one bitemporal correction checked at commit ≤ 100 distinct actions ≤ 4 MB of item bytes DynamoDB is the only durable resource one DynamoDbClient any failure: roll back, send nothing TransactWriteItems one request · every table Customer Contact Opportunity refused TXN-001 · >100 actions TXN-002 · >4 MB TXN-005 · mixed / 2PC nothing written committed every action applied, across all tables at once durable cancelled duplicate key: UniqueIndexViolation stale prior state: OptimisticLock anything else: TXN-004 nothing written outcome unknown timeout · 5xx · network TXN-003 client quarantined, request and token kept do not replay
The budget counts physical items, not business objects. Two writes to the same item in one transaction merge into one action. A correction that splits a bitemporal rectangle closes one row and inserts the pieces, so a single setQuantity as of a later date spends three actions — one close, one head, one new value — and a bounded setQuantityUntil spends four.

A transaction that goes over the cap is refused, not chunked. Two TransactWriteItems calls are two separate commits. If the second one fails, the first is already durable, and "atomic" would stop being true of your data.

CodeWhen
RELADYNAMO-TXN-001More than 100 distinct actions at commit
RELADYNAMO-TXN-002Staged item bytes over 4 MB
RELADYNAMO-TXN-003Commit outcome unknown after retries; the client is quarantined
RELADYNAMO-TXN-004DynamoDB cancelled or rejected the request, and no Reladomo conflict type applies
RELADYNAMO-TXN-005Another durable resource (JDBC, JMS) in the same transaction, a two-phase prepare, or two DynamoDB clients
RELADYNAMO-TXN-006A read reaches DynamoDB while writes are staged
RELADYNAMO-TXN-007Any non-null TxParticipationMode: no database locks or version checks through it

What a failed transaction guarantees. If the transaction is refused, rolled back or cancelled, nothing it staged is durable. A Reladomo flush does not commit anything to DynamoDB. Acceptance case 3 pins this, including a failure between closing a rectangle and inserting its replacement. There is one exception. After a timeout, a 5xx or a network error, DynamoDB may or may not have applied the request. The adapter then throws RELADYNAMO-TXN-003 with the exact request and its ClientRequestToken, and refuses to use that DynamoDbClient again until the process restarts. Check the store before you do anything else.

Workaround. Keep each unit of work well inside 100 actions, and split work at business boundaries where each part is correct on its own. A batch job that commits thousands of rows in one transaction needs a new design, not a bigger batch. For reads inside a transaction, read before the first write, or use objects Reladomo already holds. A read that reaches the store after a write is staged refuses, because returning the state from before the transaction would look like a normal answer.


Permanent

Every query starts from a key

SQL evaluates any WHERE clause and leaves indexing as a tuning decision for later. DynamoDB answers one kind of question: give it an exact partition-key value, and it can narrow by sort key. Anything else is a Scan, which reads every item in the table, and you pay for every one.

THE PREDICATE WHAT GETS READ balanceId = 42 the full partition key #17#3#88#5 #61#29#70 #42 BASE TABLE · ONE COLUMN PER PARTITION GetItem or Query · consistent read accountId = 7 a foreign key with a GSI acct 7acct 12acct 3 GSI ON accountId · ALL · EVENTUALLY CONSISTENT quantity > 5 no key, no index Scan: every item in every partition, all billed RELADYNAMO-PLAN-001 · scans are disabled unless allowTableScan(true) is configured
Partitions are placed by hash, so without the key there is no order to use. An IN list or OR on the key becomes one query per value, up to pkFanOutLimit (default 100). Past that limit the planner refuses with RELADYNAMO-PLAN-002.
strongly consistent read eventually consistent read refused by default

The planner tries three things in order: a base-table key, then a configured GSI whose partition key the predicate pins by equality, then a Scan. Scans are off by default, so some queries that run on H2 will not plan at all. That is on purpose. A silent Scan passes in testing and fails in production, on cost. There are no server-side joins either, so a deep fetch becomes extra round trips.

CodeRefused becauseWorkaround
RELADYNAMO-PLAN-001No partition key on the table or any GSI, and scans are offSupply the key, add a GSI, or enable scans for a bounded batch job
RELADYNAMO-PLAN-002Key IN/OR fans out past 100Split the call, or raise the limit
RELADYNAMO-PLAN-006More than maxPages (64) pages; a partial answer would look completeNarrow the query, or raise the limit
RELADYNAMO-PLAN-007More than 50,000 rows to hold in memoryNarrow the query
RELADYNAMO-RESIDUAL-001A predicate that follows a relationship (exists, notExists, mapped)Resolve the related keys first, then query by them
RELADYNAMO-PLAN-010computeFunction: a raw SQL expressionNone
RELADYNAMO-PLAN-011Tuple / temp-object queriesNone
RELADYNAMO-PLAN-012A float or double key componentUse an integral, string or BigDecimal key

Permanent

Rows have a ceiling

An item can be at most 400 KB. The adapter measures the item exactly as it will be stored, including the payload, pk, sk, the schema-version attribute and every GSI key, and throws ItemTooLargeException with the entity, the key and the size before sending anything. Keys are capped at 2048 bytes (partition) and 1024 bytes (sort) of UTF-8, and are checked when the key is built. A very wide row, a large text column or a binary attachment has nowhere to go.

The limits also add up. Ten items of maximum size come to 3.9 MB, so eleven of them cannot fit one 4 MB transaction however few actions they are.

Workaround. Move large values out of the row, for example into an object store, and keep a reference. That changes your object model, and the adapter has no built-in offload. For keys, the only fix is a shorter key.


Permanent

Indexes lag behind

A relational secondary index changes in the same transaction as its row. A DynamoDB GSI updates asynchronously after the write commits. A read through it can miss a row that was just committed, or return one that was just removed. DynamoDB offers no consistentRead for a GSI.

Where the planner uses a GSI:

Where it never does: GetItem and base-table queries (always consistent reads), refresh and refreshDatedObject (a consistent GetItem), date-range and delete planning, and any planner built with allowGsi(false) or inTransaction(true).

"Inside a transaction" is a setting, not something the adapter detects. Nothing sets inTransaction from the live Reladomo transaction. With the default PlannerConfig, a read inside a transaction, before its first write, can go through a GSI and get stale results. If a transactional code path reads through an index, give its persister a PlannerConfig with inTransaction(true) or allowGsi(false).

Only ALL projections are allowed. KEYS_ONLY and INCLUDE are refused at table creation with RELADYNAMO-CFG-014, because the missing attributes would decode as null. So every index stores a full copy of the item, and every write pays for that copy.

Workaround. Where correctness depends on reading your own writes, read by full key from the base table. Everywhere else, accept eventual reads on index paths. There is no way to get a strongly consistent read from an index.


Permanent

The key is the only unique constraint

A relational UNIQUE index on an email or account number rejects the second insert, whichever process sends it. The adapter enforces exactly one kind of uniqueness. Each ORM insert is conditional on attribute_not_exists(pk) for its item key, and a duplicate raises Reladomo's own MithraUniqueIndexViolationException. DynamoDB evaluates the condition, so this holds across JVMs. For a dated entity, the item key includes the temporal from-bounds.

Nothing else is unique. A GSI enforces nothing, and two items can share an index key. GsiSpec.uniqueAttribute exists, but it builds a KEYS_ONLY index, which table creation refuses with RELADYNAMO-CFG-014.

Workaround. Make the unique value its own entity with that value as its primary key, for example a CustomerEmail keyed by email, and insert it in the same transaction as its owner. A duplicate then cancels the whole TransactWriteItems with MithraUniqueIndexViolationException. It costs one action per unique value and a model change. This is the standard DynamoDB pattern, not an adapter feature, and no test here exercises it.


Not yet

Fifteen persister features refuse by name

DynamoDbPersister implements 32 Reladomo SPI methods. Seventeen of them, 15 distinct names with two overloads, throw UnsupportedOperationException with the message "DynamoDbPersister.<method> is not implemented yet for <Class>". They have no RELADYNAMO code. The method name is in the message deliberately: an empty result would look the same as an empty table. setTxParticipationMode also refuses any non-null mode, with RELADYNAMO-TXN-007.

Reladomo featureSPI methods that refuse
AggregateListfindAggregatedData
Cursor iteration and cursor-based loadingfindCursor
Full cache, runtime cache reloadloadFullCache · reloadFullCache
renewCacheForOperation
Mass delete and purge of an operation-based listdeleteUsingOperation · deleteBatchUsingOperation
findForMassDelete · prepareForMassDelete
prepareForMassPurge (×2)
Updates Reladomo merges at flushmultiUpdate · batchUpdate
Dated history that is not in cachegetForDateRange
Raw SQL functionscomputeFunction
Source-attribute routing (already refused by RELADYNAMO-CFG-012)extractDatabaseIdentifiers (×2)

Which ones a real application hits. The required set came from running applications, not from reading this list. The car-classifier and petstore demos ran with their portals bound to DynamoDB, and AcceptanceRequiredSpiTest ran with H2 disconnected and caches cold. Together those runs reached insert, find, update, count, refresh and enrollDatedObject. None of them reached a refused method.

That tells you about those applications, not yours. The rows above are everyday Reladomo APIs. Search your code first for AggregateList, deleteAll on a list, and transactions that update several objects of one class. Reladomo 18.1.0 can merge those updates into a MultiUpdateOperation or BatchUpdateOperation.

Workaround: none in the adapter. You have to change the call site. For example, aggregate in Java over a keyed query, or find the objects and delete them one at a time inside the action budget. That means your call sites do change after all.


Out of scope for 0.1.0

Migration is offline and one-way

Version 0.1.0 supports one way to migrate: stop writes to the relational source, copy the data with Backfill, verify it, then cut over. Backfill reads back and compares every row it copies, refuses an empty source unless the caller declared the source legitimately empty, and saves checkpoints so a killed run can resume. Its writes are unconditional upserts. If a stale source row is replayed after a newer DynamoDB write, it overwrites that write without warning. That is why the source must not change while the copy runs.

Three of the inspection's 13 acceptance cases are blocked by this scope, not by unfinished work.


Not yet

What nobody has measured

Every test runs against DynamoDB Local, in-process. Nothing has run against a real AWS endpoint, and nothing has run under load. DynamoDB Local is good for checking semantics. It tells you nothing about:

The only performance numbers this project has, recorded in docs/PERFORMANCE.md, are JMH micro-benchmarks of per-row encoding on one machine, one fork, three one-second iterations. Every error bar there is larger than its score. They answer one order-of-magnitude question — the codec cost is negligible next to a round trip — and nothing else.

Workaround. Before you plan a cutover, run your own preflight on a real AWS account with your model, your data volume and your concurrency.


Who this suits today

Reladynamo is a good fit for a project that:

If your project needs something outside that, it is either a permanent boundary or an open gap, and this page has named both. Finding that out in preflight costs far less than finding it after cutover.