A valid signature still needs a trust decision
An Oprindo sample had a matching asset hash and a valid signature, alongside an untrusted credential result. The useful story is in those separate answers.
An outbox preserves an unfinished obligation. A worked crash ledger shows what that buys, where duplicates remain and why the receiver needs its own boundary.
The application has accepted O42, but the warehouse has never heard of it. Both systems can be working as designed if the process stopped between saving the order and publishing its instruction. The order screen reflects the local commit; it says nothing about the missing hand-off. To recover that work later, the sender needs something durable that says not only this order exists, but also this instruction is still owed to the warehouse.
Consider a deliberately small example: order O42 must create one preparation job. Its notification is M7. These are different identities. O42 names the business object; M7 names this particular instruction about it. A future cancellation would need its own message and meaning. The immediate question is narrower: once the application accepts O42, what durable evidence says that someone still owes the warehouse M7?
Reversing the statements changes the failure window. Saving first can leave an order without publication; publishing first can leave the warehouse preparing an order whose database transaction failed. Exception handling cannot reconstruct an intention lost with the process. Recording that intention in the same local transaction as the order preserves the pending obligation, allowing recovery to continue from a known instruction rather than infer what the sender might have meant.
The outbox records that second fact in the same local transaction as the first. Insert O42 into orders and M7 into outbox with order_id O42 and sent false, then commit both or neither. The warehouse is still untouched. What has become durable is the obligation to tell it, not a claim that it already has a preparation job. That is a small promise the sender’s own database can actually keep.
A separate publisher reads committed pending rows, transmits M7 and marks it sent only after the receiving path acknowledges acceptance. This removes publication from the order transaction, but it does not remove publication work. Someone must run the publisher, notice old pending rows, deal with rejected payloads and preserve the meaning of the message. “We saved the outbox row” is a useful intermediate state, not the customer's completed outcome.
AWS’s outbox guidance describes this dual-write problem and warns about duplicates on the next hop. A table cannot make that network conversation atomic. The receiver may accept M7 just before its acknowledgement disappears, leaving the sender unable to record acceptance. M7 should remain pending in that situation. Marking it sent before transmission would make the dashboard quieter by reopening the earlier failure window: the instruction could vanish before the warehouse received it.
The publisher should therefore reuse M7 when it retries. Generating M8 because the previous attempt was inconvenient destroys the relationship between attempts and the original instruction. Retain the original payload or an explicitly versioned, sufficient event representation too. Rebuilding M7 from whatever O42 happens to look like tomorrow can quietly change what the same message identity means.
The local transaction design for O42 and instruction M7.
Follow the labelled arrows. On a narrow screen, scroll sideways if needed.
The receiving service needs its own duplicate boundary. In the downloadable model it inserts M7 into a uniquely keyed inbox and creates the O42 preparation job in one receiver transaction. A duplicate M7 finds the existing inbox row and does not repeat the job creation. The job table also has a unique order identity, making this example's one-job-per-order rule explicit rather than leaving it as a comment beside the handler.
The inbox and job constraints answer different questions. M7 in the inbox means this instruction was accepted; O42 in the job key means this order already has its one preparation job. A new M9 requesting another job is therefore not simply a redelivery of M7. The model rejects that conflict, leaving a real service to return a recorded refusal or offer an explicit alternative operation. Keep the original M7 and job when reporting it. Otherwise duplicate handling can erase the distinction between hearing the same thing twice and receiving a contradictory new instruction.
The local boundary matters again on the receiver. Recording M7 first and creating the job later would allow a crash to produce a remembered message with no job. Creating the job first and remembering M7 later leaves a repeatable effect. Both writes belong in the same receiver transaction if they share a transactional database. The sender's transaction and the receiver's transaction remain separate; there is no single commit spanning the two services.
An email, a physical label printer or an external warehouse API is outside that second database transaction. Recording a local preparation job can be made atomic with the inbox row; physically preparing the parcel cannot. Another durable hand-off or a provider-supported duplicate contract may be needed at that boundary. Calling the whole chain “exactly once” would erase the very distinctions this design makes visible.
The sequential SQLite checks use separate sender and receiver databases. Failure before sender commit leaves zero orders and zero pending messages. Commit the order and outbox, then stop before publication: there is one order, one pending message and no receiving job. That second state is the benefit to follow. The process has stopped without forgetting M7, so a later publisher has a specific obligation to resume rather than a guess about which accepted orders might need telling to the warehouse.
A receiver rollback leaves the sender’s obligation pending and creates neither inbox nor job. After a lost acknowledgement, the receiver instead has one inbox row and one job while the sender still has a pending M7. Redelivering it leaves that job count at one; acknowledgement then lets the pending count reach zero. Those local SQLite outcomes explain why pending cannot mean the warehouse definitely did nothing. The receiving record is needed to distinguish an unperformed instruction from completed work whose confirmation the sender has yet to obtain.
The broken alternatives leave different evidence. Saving O42 without an outbox row loses the instruction owed to the warehouse. Sending first can leave a receiver job after the sender’s order fails to commit. Those are different recovery problems: the first needs the missing publication obligation identified, while the second leaves a job that may need stopping because its order was never accepted.
Concurrent publishers need a defined way to claim pending rows, plus recovery for a stalled owner. The sequential model does not supply that coordination. Once creation and cancellation can both concern O42, the receiver also needs version and transition rules so an old preparation instruction cannot override a later decision merely by arriving last. Broker retention and network failures affect when those messages may reappear. The outbox keeps the publication obligation available; the receiving business contract must decide which transition a delayed instruction is still allowed to perform.
Observed states in a disposable sequential SQLite model. Sender pending means the publication obligation is still outstanding.
Scroll sideways to see all columns.
| Interruption | Order | Pending M7 | Inbox M7 | Jobs |
|---|---|---|---|---|
| Sender rollback | 0 | 0 | 0 | 0 |
| Before publication | 1 | 1 | 0 | 0 |
| Receiver rollback | 1 | 1 | 0 | 0 |
| Acknowledgement lost | 1 | 1 | 1 | 1 |
| Redelivery accepted | 1 | 0 | 1 | 1 |
There is an operational price for making the promise explicit. Pending messages require an age view, not merely a count: one old O42 may matter more than a brief surge of new work. A dashboard showing every publisher process alive cannot establish that M7 reached the warehouse. The useful closure check joins the sender's obligation to receiving acceptance, with enough identity retained to investigate a mismatch. Logs can explain an attempt; the rows explain what remains owed.
Retention needs a corresponding agreement. Deleting inbox identities while the sender can still retry old messages reopens the duplicate boundary. Keeping every payload forever creates its own storage and privacy costs. Decide the replay horizon, archive requirements and handling of unexpectedly old messages together. A deliberate refusal requiring investigation can be safer than silently treating a long-forgotten instruction as new.
If the order and its next job live in one database, an ordinary transaction may already cover the whole obligation; an optional notification might justify a simpler promise too. O42 needs the outbox because acceptance includes informing a separate warehouse. M7 lets the sender remember that unfinished work, and the receiver’s transaction separately protects acceptance and job creation. The two promises are modest enough to inspect, and together let recovery continue from what each system actually knows.
Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.
See the Payment Operations PilotPrimary sources behind this article. Living documentation may change.

Practical engineering. A human perspective.
More about Hamsa Software and the person behind it. Editorial notes.