Engineering note SempreVenda 2026
Why I rebuilt SempreVenda around a Rust transaction kernel
The first version of SempreVenda ran a real hospitality operation and proved the product’s scope. It also spread the rules of the business across roughly two hundred thousand lines of application code, which meant the answer to what a sale was depended on which part of the code you asked. This note explains what I replaced it with and why.
What the first version proved
Before writing any of it I had a list of the things a bar and café actually needs a point-of-sale to do during a service: seat and move tables, take orders, route items to the kitchen and the bar, split and merge bills, take mixed payments across cash and card, open and close a till, count stock down as it sells, hold costs against what was sold, and produce a report at the end of the night that someone can act on.
The first version did enough of all of that to show the shape of the whole product. That was the useful result. Scope questions were answered: the surface area was real, it was large, and it was buildable by one person operating the business at the same time.
What it got wrong
The problem was not any single feature. It was that the definition of a transaction lived in too many places at once.
A sale was partly defined by the screen that created it, partly by the service that priced it, partly by the code that took payment, partly by the code that moved stock, and partly by the reporting queries that read all of it back afterwards. Each of those was written at a different time, under different pressure, with a slightly different idea of the same thing. None of them was wrong on its own. Together they did not agree.
The symptom of that in an application is not usually a crash. It is a small disagreement that surfaces at the worst moment: a bill total that does not match the payment record, stock that moves twice because a retry ran the same order through a second path, a till that closes to a figure the reports will not reproduce the next morning. In hospitality those disagreements are found during service, by staff, in front of customers, and the person who has to reconcile them at the end of the night is the operator.
The deeper issue was auditability. If I could not point at one place and say this is what a sale is, then I could not confidently verify that the system was correct. And a point-of-sale that cannot be verified is a liability the moment it holds money, stock, and tax-relevant history for someone else’s business.
Why I stopped instead of continuing
The tempting move was to keep going. The feature list was long, the product was close enough to useful that finishing it felt near, and every one of those disagreements had a local fix.
Local fixes were the problem. Each one added another place where the rules lived, which made the next disagreement harder to find and the next fix more expensive. The cost of the architecture was compounding faster than the value of the features being added on top of it.
So I stopped feature development and rebuilt the transactional foundation instead. That decision cost time I did not obviously have. It is the decision I would make again, because the alternative was to keep selling a system whose correctness I could not personally vouch for.
The kernel
The replacement is a single kernel written in Rust. It holds the entire transactional model of the business: what a sale is, what a payment is, what a stock movement is, what a till session is, and what each of them is allowed to do to the others.
Everything outside the kernel asks it to do things. Nothing outside the kernel decides what happened.
The interface into it is a typed command. The result out of it is a receipt: a record of what the kernel decided, which is written before anyone is told the operation succeeded. The shape, illustratively:
// illustrative — the command surface is typed, closed, and versioned
enum Command {
OpenTable { table: TableId, covers: u8 },
AddItems { order: OrderId, items: Vec<OrderLine> },
TakePayment { bill: BillId, tender: Tender, amount: Money },
CloseTill { session: TillSessionId, counted: Money },
// ...
}
fn apply(state: &State, cmd: Command, id: CommandId) -> Result<Receipt, Rejection>
Two things follow from that signature, and they are most of the point.
First, a command that the kernel will not accept is rejected explicitly, with a reason, and changes nothing. There is no partially applied sale. Validation is not spread across the screens that happen to submit the command; it belongs to the model.
Second, because a command carries its own identifier, the kernel can recognise one it has already applied. That is what makes the system safe to retry.
Idempotency and retry
A terminal in a bar loses its connection, gets carried into a back room, runs out of battery mid-order, and is used by someone in a hurry who taps the button twice because the first tap did not appear to register. Retries are not an edge case in this product. They are the normal operating condition.
Every command is submitted with an identifier generated at the point of intent. If the kernel has already applied that identifier, it returns the original receipt rather than applying the command again. The second tap produces the same answer as the first. A client that never received a response can resend without risk, which means the client is allowed to be simple: submit, wait, resend if unsure.
This is also what makes offline operation tractable. A terminal that cannot reach anything else can still record intent locally, apply it against its own kernel state, and reconcile later without the reconciliation itself producing duplicates.
Crash behavior
Power fails. Tablets are dropped. Processes are killed by an operating system that decided something else needed the memory. The question is not whether it happens but what the system is permitted to look like afterwards.
The rule in the kernel is that a receipt is durable before it is acknowledged. If the process dies after the receipt is written and before the caller hears back, the caller retries with the same command identifier and receives the receipt that already exists. If it dies before the receipt is written, the command did not happen and the caller retries into a clean state. There is no third outcome where half of a sale survives.
Stated plainly: after a crash the system is in a state some sequence of accepted commands could have produced. Recovery is replay, not repair.
Audit history
The audit trail is written, not reconstructed. This is the distinction that mattered most to me as an operator.
In the first version, the history of a service was something you assembled afterwards by querying current state and inferring how it got there. That works until two records disagree, at which point you are guessing. In the kernel the accepted commands and their receipts are the append-only record; current state is what you get by folding that record forward. Nothing is edited in place. A correction is a new entry that says what was corrected and why, and the original stays where it was.
That gives one answer to the question an operator actually asks at the end of the night, and the question an accountant asks months later: what happened, in what order, and who did it.
- 01Typed commandsA closed, versioned set of operations. Anything not expressible as a command cannot be done to the business state.
- 02Validation in one placeA command is accepted or rejected with a reason. Rejections change nothing.
- 03ReceiptsThe kernel’s decision is recorded durably before the caller is told it succeeded.
- 04IdempotencyA repeated command identifier returns the original receipt instead of applying twice.
- 05Defined crash behaviorEvery post-crash state is a state some accepted command sequence could have produced.
- 06Append-only historyCorrections are new entries. Nothing is edited in place.
- 07Reduced authority in the UIThe interface submits intent and renders results. It does not decide outcomes.
Where the interface sits now
The largest practical change is how little the application layer is allowed to do. It orchestrates: it decides which command to submit, when, and what to show while waiting. It does not price, it does not validate against business rules, and it does not write history.
That is a smaller job than the first version’s interface had, and a much easier one to get right. It also means a second interface — a kitchen display, a handheld, a reporting view — cannot introduce a competing definition of what a sale is, because it has no way to express one.
What is left
The kernel is finished. The production application is being rebuilt on top of it, feature by feature, in the order a service needs them: tables and orders, production routing, payments, till and cash management, stock and costs, then reporting.
I am running the result in my own bar and café as it goes, which is the only test I trust for this category of software. When a rebuilt feature is wrong, I find out during a service, standing behind the counter, the same way the first version taught me what the product had to be.
Agnaldo Guerra Southern Brazil 2026