The snack machine died before ACK
The Order Was Saved, but the Delivery Intent Was Lost
In one line
Saving the work and notifying another service are different facts, so you record the intent that still has to be delivered together with the work.
Why this was needed
The snack machine in the previous module saved the stock and the cursor together in one SQLite file. This time the order receiver and the warehouse are separate. The receiver takes an order, and the warehouse receives that order and increases the quantity prepared for shipment. 7 orders were recorded in the intake DB, but nothing arrived at the warehouse. The operator answers that it succeeded because the order table looks fine, but from the user's point of view the snacks were not prepared. A single database transaction does not automatically tie two systems together.
You might think, "Can't we just call HTTP on the line after the DB commit?" The process can end before it runs that very next line. If you swap the order and send first, the opposite problem arises. The warehouse may have prepared the goods while the order DB commit fails. Swapping the order of two lines cannot remove the failure window in between. In this module you first design where the evidence of success is left and where the work to retry is left.
How it works
Make the intent to send atomic, not the send
The receiver records orders and outbox together in one transaction. orders is the work it accepted, and outbox is the item to be delivered later. Both must be saved or both must disappear. After COMMIT, a separate delivery loop reads the unfinished items in the outbox and sends them to the warehouse. The function that stores the order does not itself wait for an HTTP response. This is so that, even when an external system is slow, it does not hold the DB write lock for the whole network wait.
접수 DB: BEGIN → orders 삽입 → outbox 삽입 → COMMIT
전달 루프: 미완료 조회 → HTTP 요청 → 업무 ACK 확인 → sent=1
창고 DB: BEGIN → inbox 삽입 → stock 갱신 → COMMIT → ACK
In this picture, the only atomic parts are between each DB's BEGIN and COMMIT. The whole arrow sequence is not a single transaction. Thanks to the outbox, even if the receiving process dies right after the commit, the restarted delivery loop can find the unfinished intent. But the response can vanish after the warehouse applied it, so a duplicate send is still possible. You must distinguish "we do not lose the work to send" from "the send happens exactly once".
Mark success and uncertainty on a timeline
For example, you enter an order with ID snack-7 and quantity 7. If it exits after the orders insert, both orders and outbox must have 0 rows from an independent connection. The same holds when the outbox insert has happened but the commit has not. If it exits after the commit, both must have 1 row. In that last case, if you delete the order just because the caller did not get a response, you are performing a separate act of canceling work that was already accepted. You should resubmit with the same ID and content and check the existing acceptance.
In the test, you do not only check that the change is visible within the same connection. A connection that has not yet committed can read its own changes. You also have to compare what a separate SQLite connection reads to know the state that is finalized externally. Also separate the case where try/finally cleans up an error from the case where os._exit means the cleanup code itself never runs. The former shows the correctness of exception handling, and the latter shows the state you can read from the database file after the process is gone.
Choosing the ID is a business design matter
In this lab, the ID is a string of 1–64 characters of ASCII letters, digits, underscores, and hyphens, and the quantity is an integer from 1–1000. True can look like an integer in Python, but it is not accepted as a quantity. This restriction is a teaching contract, not the standard of a real order service. The same ID and the same quantity is a retry, the same ID and a different quantity is a Conflict, and a different ID with the same quantity is handled as a separate order. If price, customer, or product type is added, you have to decide again which fields are the content of the same work.
If you generate a fresh ID every time from the creation time or the current time, a retry of the same order becomes a different order. Conversely, if you use the quantity itself as the ID, two people who happened to order 7 get merged into one. The number of network reconnects, the request time, and the business identifier each mean something different. This implementation starts from the premise that the caller has already decided on a stable business ID. We do not claim to have built an ID issuing service or an authentication system for multiple users.
What it looks like in the field
When an FDE connects a customer's existing DB to a new notification or job system, the situation "one side went through and the other did not" comes up. Requirements usually describe only the success scenario, so you have to put interruptions before and after the commit, before and after the send, and before and after the ACK into the acceptance tests. The two SQLite files in this lab are a device for reproducing those boundaries in small form. They are neither evidence of an atomic commit with a real payment provider nor of having built a message broker.
Using an outbox also brings operational responsibilities. You have to watch how old the unfinished rows are, whether the delivery loop is running, and whether a failing item blocks the ones behind it. Deleting unfinished rows to make the count 0 is not recovery. When you do not know what was received, you must design the resend and confirmation first. A table that piles up forever with no retention period or capacity limit is not a finished operational design either. This lab verifies the failure semantics on a small amount of data and leaves long-term retention and cleanup as the next design problem.
What you will do in the next check
In the quiz right after this, you judge the table state before and after the commit and the failure of the two send orders. In the next module, you store the inbox and the stock together so that even if the delivery loop sends the same item again, the warehouse does not repeat the business effect. In the last lab, you confirm each interruption point with a real process termination.
Further reading in the official docs
- AWS Transactional outbox: Read about the dual-write problem between a DB and event delivery and the considerations for duplicate receipt.
- SQLite Transaction: Check the boundaries of explicit BEGIN, COMMIT, and ROLLBACK.