Ground Truth

Moving An Allocation Is A Transition, Not An Edit To Two Quantities

Mostafa DhouibMostafa Dhouib·10 min read
The short answer

Shifting an allocation from one commitment to another moves real exposure, so it has to be recorded as its own transition with its own entry. Implemented as an edit to two numbers it leaves nothing to reconstruct, which means the move cannot be explained when it is challenged and cannot be made safe when two people do it at once. The operation is the record, not the resulting quantities.

Moving An Allocation Is A Transition, Not An Edit To Two Quantities

The short answer. When you move an allocation from one commitment to another, you have moved real exposure between two obligations. If the implementation is a decrement on one quantity and an increment on another, the only thing left afterwards is two numbers that happen to add up. There is no record that a move occurred, no reason, no actor, and nothing to reconstruct when the move is challenged weeks later. Recorded instead as a single reallocation transition with its own entry naming source, destination, amount, actor and reason, the quantities become derived values and the move becomes a fact you can explain.

A platform tracks positions against commitments. Volume is allocated to a contract, a cargo, a delivery window, a counterparty.

Then something changes and an allocation has to move. A delivery slips, a buyer takes less than expected, a better home appears for the tonnage. Someone shifts part of one allocation onto another commitment.

The straightforward implementation is two updates. Reduce the quantity on the source, increase it on the destination. The totals still balance and the screen shows the right numbers. That implementation loses the only thing worth keeping.

What the edit destroys

Think about what exists the moment after the two updates commit.

The source commitment holds a smaller number. The destination holds a larger one. Nothing anywhere says that one caused the other. Nothing says who did it, when, or why. The previous values are gone, because an update in place does not keep what it replaced.

So the questions that get asked later cannot be answered.

Why is this commitment short. Because somebody moved volume off it. Which movement, and to where. Unknown, because the two edits were never linked. Was that movement authorised. Unknown. Did it happen in one action or in four separate ones over a fortnight. Unknown, because the current value is the only surviving evidence and it cannot distinguish one move from many.

The same problem shows up in a more immediate way. If the source update succeeds and the destination update fails, volume has vanished. The totals no longer balance and there is no record of what the intended operation was, so there is nothing to retry and nothing to compensate. A reconciliation job can tell you the books are out. It cannot tell you by what.

Audit logs do not rescue this. A log that records "quantity field changed from 500 to 300" is a record of a field, not of an operation. It cannot say that this decrement and that increment were one decision, because the application never expressed them as one.

Two quantity updates
What survives the commit
two numbers
the previous values are gone, because an update in place does not keep what it replaced
Why is this commitment short
unanswerable
the current value cannot distinguish one move from four
If the second write fails
volume vanishes
nothing to retry and nothing to compensate, because the intended operation was never expressed
What a field level audit log holds
quantity changed from 500 to 300
a record of a field, not of an operation
A retried request
applies another balanced pair
it balances perfectly and is completely wrong
One recorded transition
Source, destination, amount, actor, reason, key
one entry
written append only, with quantities derived from it
Atomic by construction
no halves
either the reallocation exists or it does not
Both legs inseparable
one entry, two views
the source history shows where volume went and the destination shows where it came from
Reversal
another entry referencing the first
the history shows a move and a correction, which is what happened
Idempotency
comes for free
one transition with one key resolves a double tap to one move
the operation is the record
Resist adding a general purpose set quantity operation alongside this. The moment it exists it will be used, and every use is a number with no provenance inside an otherwise reconstructable history.
FigureA decrement on one commitment and an increment on another leaves two numbers that happen to add up and nothing stating that one caused the other. Recorded instead as one append-only transition naming source, destination, amount, actor and reason, the quantities become derived values and a partial failure stops being representable.

Reallocation as a first-class operation

The alternative is to name the operation and record it.

A reallocation is one transition. It carries: the source commitment, the destination commitment, the amount, the actor, the time, a reason code, and an idempotency key. It is written as a single append-only entry. The quantities on commitments are not stored at all, or if they are stored for query performance, they are a projection derived from the entries and rebuildable from them.

Several things follow directly.

The operation is atomic by construction. There is one write, not two, so a partial failure is not representable. Either the reallocation entry exists or it does not. You cannot lose volume between two halves of a move, because there are no halves.

Both legs are inseparable. The entry names both ends. Anyone reading the source commitment's history sees where the volume went, and anyone reading the destination sees where it came from, because both views derive from the same entry.

The reason is captured at the moment it is known. A reason supplied when the move is made is accurate. A reason reconstructed later from a chain of emails is a guess, and in my experience it is usually the guess of whoever is least invested in the answer.

A reversal is another entry. If the move was wrong, you record a reallocation in the opposite direction, referencing the original. The history then shows a move and a correction, which is what happened, rather than a quantity that silently returned to its earlier value.

Idempotency comes for free. One transition with one key means a retried request, a double-tap or a replayed message resolves to one move rather than two. Under the two-edit design the retry applies another pair of increments and decrements, which balances perfectly and is completely wrong.

The design discipline is to resist adding a general-purpose "set quantity" operation alongside this. The moment that exists, it will be used, and every use is a quantity with no provenance sitting in the middle of an otherwise reconstructable history.

Consistency when two people reallocate at once

Allocation has a constraint that balances do not always have. You usually cannot allocate more than exists, and you cannot move more off a commitment than is allocated to it. That makes concurrent reallocation a correctness problem rather than a performance one.

The specific hazard: two users each move volume off the same source commitment at the same time. Each reads a source with 500 available. Each moves 400. Each move is individually valid against what it read. Together they take 800 off a commitment that had 500.

  1. Two users open the same source commitment
    each reads 500 available
  2. Each decides to move 400
    individually valid against what each one read
  3. Application code validates before opening a transaction
    this is the shape of the bug, not a defence against it
  4. Both entries commit
    800 moved off a commitment that held 500
  5. Compute availability inside the writing transaction
    under an isolation level or a lock that makes the computation valid at commit time
  6. Serialise per commitment and order locks by identifier
    contention stays on the contract people are fighting over, and two opposing moves wait instead of deadlocking
  7. Retry on abort, safely
    normal operation under serialisable isolation, and only safe because the entry carries an idempotency key
Never let the projection be the arbiter. A read model that is slightly behind will approve an over-allocation that looked correct when it was validated.
FigureMaking both legs one entry does not fix the read then write race, because the hazard is in when the constraint is evaluated rather than in how many writes there are. Each move is individually valid against what its author read, and together they take more off the commitment than was ever on it.

This is the classic read-then-write race, and the fact that both legs are now one entry does not solve it on its own. What solves it is enforcing the invariant at the write.

Check the constraint inside the transaction that writes the entry. Compute the source's available amount and insert the entry in the same transaction, under an isolation level or a lock that makes the computation valid at commit time. Validating in application code before opening a transaction is the shape of the bug, not a defence against it.

Serialise per commitment, not globally. A lock on the source commitment row, or a conditional insert that fails when the derived available amount is insufficient, confines contention to the commitment people are actually fighting over. A global lock on allocation makes the whole system as slow as its busiest contract.

Acquire locks in a consistent order. A reallocation touches two commitments, so two concurrent moves in opposite directions between the same pair can deadlock. Order the locks deterministically, for instance by identifier, so both transactions take them in the same sequence. Then one waits instead of both failing.

Expect retries and make them safe. Under serialisable isolation, transactions will be aborted and must be retried. That is normal operation, not an error, and it is only safe because the entry carries an idempotency key, which means a retry that actually did commit before the client lost the response does not apply twice.

Do not let the projection be the arbiter. If the stored quantity is a read model, it may be slightly behind. Validating availability against it allows an over-allocation that looks correct at the time. The constraint must be checked against the authoritative entries.

The question somebody will ask

The reason this matters is not architectural taste. It is that the operation will be challenged.

A counterparty disputes a short delivery. An auditor asks why a contract was under-allocated at month end. A regulator or an internal reviewer wants to know whether a position was moved to make a figure look different at a reporting date. Each of those is the same question: show me how this got here.

With reallocation as a recorded transition, the answer is a list. On this date, this person moved this volume from this commitment to that one, for this reason, and here is the transition that recorded it. The current quantity is derived, so the derivation can be shown alongside it.

With the two-edit implementation, the answer is the current number and an explanation assembled from memory. That is not a worse answer. It is a different kind of thing. It is testimony rather than evidence, and the difference becomes very clear in the conversation where it matters.

There is a second-order benefit. A system that records why volume moved accumulates a dataset about the business: which commitments are habitually over-promised, which counterparties cause moves, how often slippage drives reallocation. A system that overwrites quantities throws that away as a side effect of answering one question slightly faster.

The honest limit

An append-only allocation history is not cost-free. Current position becomes a derived value, so you will build and operate projections, and every screen needs a stated tolerance for staleness. Querying a position by replaying entries is slower than reading a column, which is why the projection exists, and keeping the projection correct becomes its own discipline including a rebuild path you have actually run.

The constraint enforcement is where most of the remaining risk lives. Serialisable isolation with retries is correct and costs throughput, and under heavy contention on one commitment users will see conflicts. That is the honest trade: visible contention instead of silent over-allocation.

Reason codes also degrade in practice. A fixed list is fast to pick from and tends to collapse into whichever option is first, while free text is accurate and unanalysable. A short list with a mandatory note on the least specific option is the least bad compromise I have found, and it still needs occasional review of what people are actually selecting.

Finally, recording the move perfectly does not make the move right. If someone reallocates volume they should not have, the system faithfully records an authorised-looking transition. What this design guarantees is that the move is visible and attributable, not that it was correct. Those are different controls and both are needed.

FAQ

What is actually lost if reallocation is implemented as two quantity updates? The operation itself. Afterwards there are two numbers that add up and nothing stating that one caused the other, who did it, when, or why. The previous values are gone because an update in place does not keep what it replaced. So the move cannot be reconstructed, and a partial failure leaves volume missing with no record of the intended operation to retry.

Doesn't an audit log of field changes cover this? No. A log entry saying a quantity changed from one value to another records a field, not an operation. It cannot link a decrement on one commitment to an increment on another as a single decision, because the application never expressed them as one. The reason and the actor attach to the move, which the log has no concept of.

How should the reallocation entry be shaped? As one append-only entry naming the source commitment, the destination, the amount, the actor, the time, a reason code and an idempotency key. One write rather than two means a partial failure is not representable, both legs are inseparable, and a retried request resolves to a single move instead of applying another balanced pair of changes.

What stops two people moving the same volume at the same time? Enforcing the constraint inside the transaction that writes the entry, rather than in application code beforehand. Each user reads the same availability and each move looks individually valid, so the invariant has to hold at commit time. Lock or serialise per commitment so contention stays local, and order locks deterministically so two opposing moves between the same pair do not deadlock.

Can availability be checked against the stored quantity? Not safely, if that quantity is a projection. A read model can be slightly behind, so a check against it can approve an over-allocation that looked correct when it was validated. The constraint has to be evaluated against the authoritative entries inside the writing transaction.

How do you undo a reallocation that should not have happened? Record a reallocation in the opposite direction, referencing the original entry and carrying its own reason. The history then shows a move followed by a correction, which is what occurred. Deleting or editing the original hides the fact that anything happened and leaves a quantity that appears never to have changed.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program