Reladynamo · the price, stated
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.
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 lose | Kind | Workaround |
|---|---|---|
| Units of work over 100 items or 4 MB | Permanent | Split into units that are each correct on their own |
| Reading your own uncommitted writes from the store | Permanent | Read before the first write, or use objects already in the cache |
| Queries on any predicate | Permanent | Supply the key, add a GSI, or opt in to Scan |
| Items over 400 KB, oversized keys | Permanent | Move large values out of the row |
| Read-your-writes through an index | Permanent | Read by full key instead. Index reads: none |
| Unique constraints off the primary key | Permanent | Model the unique value as its own keyed entity |
| 15 persister features: aggregates, cursors, mass delete… | Not yet | None. Rewrite the call site |
| Online migration, the way back, ASE | Out of scope | Offline copy with a write freeze |
| Evidence from real AWS and real load | Not yet | Run your own preflight |
Permanent
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.
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.
| Code | When |
|---|---|
| RELADYNAMO-TXN-001 | More than 100 distinct actions at commit |
| RELADYNAMO-TXN-002 | Staged item bytes over 4 MB |
| RELADYNAMO-TXN-003 | Commit outcome unknown after retries; the client is quarantined |
| RELADYNAMO-TXN-004 | DynamoDB cancelled or rejected the request, and no Reladomo conflict type applies |
| RELADYNAMO-TXN-005 | Another durable resource (JDBC, JMS) in the same transaction, a two-phase prepare, or two DynamoDB clients |
| RELADYNAMO-TXN-006 | A read reaches DynamoDB while writes are staged |
| RELADYNAMO-TXN-007 | Any 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
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.
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.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.
| Code | Refused because | Workaround |
|---|---|---|
| RELADYNAMO-PLAN-001 | No partition key on the table or any GSI, and scans are off | Supply the key, add a GSI, or enable scans for a bounded batch job |
| RELADYNAMO-PLAN-002 | Key IN/OR fans out past 100 | Split the call, or raise the limit |
| RELADYNAMO-PLAN-006 | More than maxPages (64) pages; a partial answer would look complete | Narrow the query, or raise the limit |
| RELADYNAMO-PLAN-007 | More than 50,000 rows to hold in memory | Narrow the query |
| RELADYNAMO-RESIDUAL-001 | A predicate that follows a relationship (exists, notExists, mapped) | Resolve the related keys first, then query by them |
| RELADYNAMO-PLAN-010 | computeFunction: a raw SQL expression | None |
| RELADYNAMO-PLAN-011 | Tuple / temp-object queries | None |
| RELADYNAMO-PLAN-012 | A float or double key component | Use an integral, string or BigDecimal key |
Permanent
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
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:
estimatedVersionsPerKey is above 4. That threshold has not been calibrated.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
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
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 feature | SPI methods that refuse |
|---|---|
AggregateList | findAggregatedData |
| Cursor iteration and cursor-based loading | findCursor |
| Full cache, runtime cache reload | loadFullCache · reloadFullCache renewCacheForOperation |
| Mass delete and purge of an operation-based list | deleteUsingOperation · deleteBatchUsingOperation findForMassDelete · prepareForMassDelete prepareForMassPurge (×2) |
| Updates Reladomo merges at flush | multiUpdate · batchUpdate |
| Dated history that is not in cache | getForDateRange |
| Raw SQL functions | computeFunction |
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
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.
Backfill copies rows that
something else has already extracted. Nothing has run against ASE, and passing on H2 proves
nothing about ASE temporal types, collation or identity columns. Workaround: none from this
project.Three of the inspection's 13 acceptance cases are blocked by this scope, not by unfinished work.
Not yet
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:
TransactionConflict rates with concurrent writers;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.
Reladynamo is a good fit for a project that:
ALL-projected GSI, and can
tolerate eventual consistency on those index reads;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.