Moving An Allocation Is A Transition, Not An Edit To Two Quantities
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.
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.
- Two users open the same source commitmenteach reads 500 available
- Each decides to move 400individually valid against what each one read
- Application code validates before opening a transactionthis is the shape of the bug, not a defence against it
- Both entries commit800 moved off a commitment that held 500
- Compute availability inside the writing transactionunder an isolation level or a lock that makes the computation valid at commit time
- Serialise per commitment and order locks by identifiercontention stays on the contract people are fighting over, and two opposing moves wait instead of deadlocking
- Retry on abort, safelynormal operation under serialisable isolation, and only safe because the entry carries an idempotency key
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.
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