Skip to content

Foundations ​

These are the eight rules every TMS v2 feature is built on. For each rule this page gives what it guarantees to the business, the v1 problem it answers (with the PROD count, read in Oct–Nov 2026), whether it is built yet, and what a spec has to state because of it.

Read this before designing or reviewing any feature. Each rule was written against a v1 failure measured on PROD, and a feature that skips one tends to repeat that failure. The overview of the system is in TMS v2 at a glance, and the full v1 comparison is in v1 vs v2.

#ruleguaranteesstate
1One business action is one all-or-nothing stepno half-done orders, tasks or plansBuilt for DO intake and outbound events; Designed elsewhere
2Each fact lives in one placeno copies that disagreeDesigned; partly in the prototype
3Only the owner writes a recordrules cannot be bypassedDesigned; the prototype still exposes some generic writes
4A repeat changes data onceno double orders, double clock-ins, double cashBuilt for Kafka intake and jobs; Designed elsewhere
5Every side effect that must not be lost is a saved rowno lost message to OMS, no lost email or jobBuilt
6Behaviour lives in code, settings are checked dataone admin edit cannot silently break a ruleBuilt for settings and schedules; Designed for status tables
7Every record reaches an endnothing stays open foreverDesigned
8One history, and every log has an expiryhistory you can search, a database that does not grow without limitBuilt

1. One business action is one all-or-nothing step ​

A business action writes everything it touches in a single database transaction: the record, its history row and its outgoing event. The write also checks that the record is still in the status the action expects. If anything fails, nothing is written. If someone else changed the record first, the action is refused and can be retried.

v1 problem. v1 saved one action as several separate writes that approved one another and swallowed errors, so a crash left the action half done. On PROD, 3,720 DOs sit IN_LOAD with no live task, 92 prebuilds are stuck GENERATING and 20 delivered COD orders have no cash record.

A spec must state: the action's name, the status it expects at the start, and every record it writes.

2. Each fact lives in one place ​

The DO is the only place for an order's status and its COD amount. Routes, stops and reports refer to the DO by id and read it when they need it. A copy is allowed only as a labelled snapshot that nobody updates afterwards, such as the store's name printed on a document.

v1 problem. v1 copied the whole DO into each stop, kept links on both sides and stored totals and report tables, and the copies drifted apart. On PROD there are 2,968 delivered stops on tasks still in transit, 613 orders whose cash is counted twice, 99 of 561 driver-vehicle links that are one-sided, and 1.28M copied report rows.

Cost. A screen that shows a stop with order details has to read the DO as well. Reports read live tables, so they need the right indexes.

A spec must state: which record owns each field it shows or changes.

3. Only the owner writes a record ​

Each business area owns its records (the table is in TMS v2 at a glance). Work records such as a DO, a plan, a duty record or a cash handover change only through the owner's commands, and each command enforces the owner's rules. Another area calls that command in-process. It never uses a generic create/update endpoint and never makes an HTTP call back into the system. Master data (stores, vehicle types) keeps plain create/update screens.

v1 problem. v1's generic routes let any client PATCH stops and proofs of delivery, and partner scope and PIN checks were only data. Any employee's PIN at any store could confirm a cash handover.

Today. The prototype still lets REST create, update and delete DO lines. The design allows them to change only through intake and execution commands.

A spec must state: which area owns each record, and the command it calls.

4. A repeat changes data once ​

Phones resend, users double-tap, Kafka redelivers and pods restart. Every write therefore carries something that marks a repeat:

source of repeatshow v2 recognises itstate
Kafka message from OMSmessage id remembered for 7 daysBuilt
background joba unique key per job, so enqueueing the same job twice gives one rowBuilt
tap in the driver app (clock-in, deliver, upload)a request id or client upload id that the server checksDesigned
plan generate, cash confirma conditional write on the expected status (rule 1)Designed

The target is that the same command sent 20 times at once across two server instances changes the record once.

v1 problem. v1 had no unique key or event id on messages, taps or runs. A double tap clocked a driver in twice, a POD retry created a second POD (5,579 DOs in 30 days), and 555 of 2,414 store-date pairs ran more than one prebuild in 30 days.

A spec must state: what makes two requests "the same", and what the second one returns.

5. Every side effect that must not be lost is a saved row ​

Anything that leaves the database (a Kafka message to OMS, an email, an export, a planning job) is written as a row in the jobs table inside the same transaction as the business change. A worker picks it up, holds it under a time-limited lease and retries it with growing waits. If the retries run out it is marked DEAD, which shows in admin with a retry button. Messages for one DO are sent in order, and a DEAD one stops that DO's later messages until someone retries or skips it, so OMS never sees statuses out of order. Each message carries an event id that the receiver can use to drop duplicates.

Work that may be lost (a push notification, an OTP, a timer that is checked again anyway) goes to a lighter Redis queue instead.

v1 problem. v1 sent inline and swallowed failures. A failed send was logged but never resent, and every handler had zero retries. About 16 times a day the pipeline broke before sending and left no Kafka log at all, so the loss was invisible. Every message opened a new Kafka connection. Schedules missed while pods were down never caught up, and a pod that died mid-job left its row stuck until someone reset it by hand.

Gain. For the same daily work v2 writes about 190k rows to the database instead of v1's ~520k.

Cost. Delivery is at least once, so OMS and the staff app must drop duplicates by event id, and we have to tell them. One stuck message holds back that order's later updates until ops act on it.

A spec must state: each side effect, and whether it may be lost or must not be.

6. Behaviour lives in code, settings are checked data ​

Status tables, flows and what triggers what are written in code and reviewed like code. Ops still change behaviour through data where that is safe:

ops can changehow
settings (radius, retention days, keys)typed settings, validated on save, with the history of who changed what; secrets encrypted
schedulesdeclared in code; admin can switch one off or change its timing
templates, export columnsdata, with read-only scripts run in a sandbox
a wrong status on a recorda correction command declared in code: needs ops-admin rights and a reason, fixes the side effects too, and is audited

There is no free "set any status" button as in v1. A case no correction command covers is fixed with a data-fix script written by a developer.

v1 problem. In v1, workflows, layouts and schedules were live data that anyone with admin rights could edit, with no version and no check. 109 of 112 entities carry live layouts. Deleting one date-tracking field stopped cancelled orders getting their cancel date for about two months before anyone noticed.

Cost. A new status move or a new schedule needs a release, and ops lose the free status button.

A spec must state: the status table with every allowed move, and the correction commands ops need.

7. Every record reaches an end ​

Every record that opens has a defined way to close, and someone is told when it does not:

recordhow it ends
duty record (driver's day)day close ends it, even if the driver never clocked out
order in HOLDthe store decides at return; if not, it goes on the ops follow-up list 3 days after the task date (rule waiting on BigC)
task never startedexpires after its date and its orders go back to UNASSIGNED; whether it cancels itself or only shows on a list waits on BigC
ops hold on a DOreleased automatically when the DO reaches a final status, goes back to UNASSIGNED, or is taken off its task before departure
cash handover with a shortageneeds a reason before it closes
failed Kafka message, failed jobDEAD, visible in admin until someone acts

v1 problem. In v1 nothing closed on its own. On PROD, 1,348 DOs are still TO_ATTEMPT a week later, 66,087 shift rows are still ACTIVE after their date, and there were 7,727 holds in 30 days.

A spec must state: how each record it opens is closed, and who sees it if it is not.

8. One history, and every log has an expiry ​

Every business action writes one history row in the same transaction: who did it, through which channel, the reason, and the before and after values. One collection holds all of it, indexed by record and by person, so every record in admin has a history tab. History is kept 13 months until BigC confirms otherwise.

Technical logs (Kafka, Google, outside requests) each have a retention setting between 1 and 365 days, defaulting to 60. A finished job's row is deleted at once, and its trace stays only in the application logs. Detailed request logging switches on only for a rule an admin sets, and that rule expires within 72 hours. Files go straight from the client to S3 and never pass through the server.

v1 problem. v1 wrote a history row for every save, including saves of its own log tables, and ended up with 48.7M rows indexed only by id, so searches time out. kafka_logs alone holds 7.41M rows.

Cost. A job that succeeded cannot be opened in admin afterwards, only found in the logs.

A spec must state: the business actions that need a history row, and the reason field if one is required.

Sizing the design aims for ​

These are the numbers specs and load tests are checked against.

areatarget
order volume10,000 order messages a day (about twice today's ~4,900)
planning1,500 orders per store per day
outbound eventsabout 654k a week (three times today's ~218k)
speedan order becomes a DO within 5 s; optimise is accepted in under 1 s; a report's first page loads in 2 s for a 31-day range
recoverya process killed mid-step leaves all or nothing and finishes once on retry
security0 rows of another partner from any endpoint; PIN and OTP lock after 5 wrong tries