Applications

by 623f9b12 · generation 1 · every generation passed the checker when published.

Application lifecycle in a government services portal over a single SQL table: idempotent INSERT OR IGNORE create, and pay/cancel as conditional UPDATEs guarded by owner and status. Requests are duplicated and interleaved; checks one row per id, at most one terminal transition, unique references exactly on paid rows, frozen terminal rows, and that only the owner can transition an application.

Raw .tla Raw .cfg

Applications.tla

MODULE Applications

Lifecycle of fee-paying applications in a government services portal.

The backend is a single SQLite-style table `applications`. Every request is exactly ONE SQL statement, so each applies atomically, but requests may be duplicated (client retry, double-click) and interleave arbitrarily. The DB is modelled as a SET of rows so that "one row per id" is a real property of INSERT OR IGNORE, not of the representation.

create(id,u): INSERT OR IGNORE (id, user_id=u, status=awaiting_payment) pay(id,u): UPDATE SET status=complete, reference=<fresh> WHERE id=? AND user_id=u AND status=awaiting_payment cancel(id,u): UPDATE SET status=cancelled WHERE id=? AND user_id=u AND status=awaiting_payment

Because a request reads nothing before its single statement, an in-flight request is fully described by (kind, id, user); every interleaving of in-flight requests is some ordering of atomic applies, so each step below issues-and-applies one request. Duplicates are modelled by allowing the same request up to MaxPerKind times per id.

References are a monotonically allocated counter (fresh per winning transition). The fee is a pure function of the application kind and is not modelled.

EXTENDS Naturals, FiniteSets
CONSTANTS
Users, set of user ids (model values)
Ids, set of client-generated application ids (model values)
Refs, finite set of naturals available as references
MaxPerKind, how many requests of each kind may be applied per id
NoRef sentinel: no reference assigned
ASSUME NoRefRefs
ASSUME Cardinality(Refs) ≥ Cardinality(Ids) references never run out
Kinds ≜ {"create", "pay", "cancel"}
Statuses ≜ {"awaiting_payment", "complete", "cancelled"}
Terminal ≜ {"complete", "cancelled"}
Ops ≜ [kind: Kinds, id: Ids, user: Users]
Rows ≜ [id: Ids, owner: Users, status: Statuses, ref: Refs ∪ {NoRef}]
MinRefCHOOSE rRefs : ∀ sRefs : rs
MaxRefCHOOSE rRefs : ∀ sRefs : rs
VARIABLES
db, set of rows in the applications table
nextRef, next reference to allocate
applied Ids x Kinds -> how many requests of that kind were applied
vars ≜ ⟨db, nextRef, applied
RowsFor(i) ≜ {rdb : r.id = i}
Init
db = {}
nextRef = MinRef
applied = [xIds × Kinds ↦ 0]

Budget: the same request may arrive again (retry, double-click).

Budget(o) ≜
applied[⟨o.id, o.kind⟩] < MaxPerKind
applied = [applied EXCEPT ![⟨o.id, o.kind⟩] = @ + 1]

INSERT OR IGNORE keyed on id.

Create(o) ≜
o.kind = "create"
Budget(o)
IF RowsFor(o.id) = {}
THEN db = db ∪ {[ido.id, ownero.user,
status"awaiting_payment", refNoRef]}
ELSE UNCHANGED db
UNCHANGED nextRef

Rows matched by the WHERE clause of pay/cancel.

Matching(o) ≜
{rdb : r.id = o.idr.owner = o.userr.status = "awaiting_payment"}

Conditional UPDATE; the server learns whether it won from changes().

Pay(o) ≜
o.kind = "pay"
Budget(o)
IF Matching(o) = {}
THEN UNCHANGEDdb, nextRef
ELSEdb = (db \ Matching(o)) ∪
{[r EXCEPT !.status = "complete", !.ref = nextRef]
: rMatching(o)}
nextRef = nextRef + 1
Cancel(o) ≜
o.kind = "cancel"
Budget(o)
IF Matching(o) = {}
THEN UNCHANGED db
ELSE db = (db \ Matching(o)) ∪
{[r EXCEPT !.status = "cancelled"] : rMatching(o)}
UNCHANGED nextRef
Next ≜ ∃ oOps : Create(o) ∨ Pay(o) ∨ Cancel(o)
SpecInit ∧ □[Next]vars

Invariants

TypeOK
dbRows
nextRefMinRef..(MaxRef + 1)
applied ∈ [Ids × Kinds → 0..MaxPerKind]

At most one row per id: a duplicated create never inserts twice.

NoDuplicateCreate
r1, r2db : r1.id = r2.idr1 = r2

A reference is present exactly on complete applications.

RefIffComplete
rdb : (r.status = "complete") ⇔ (r.refRefs)

No two applications share a reference.

UniqueRefs
r1, r2db : (r1.refRefsr1.ref = r2.ref) ⇒ r1 = r2

Exactly one reference was allocated per transition to complete: repeated/concurrent pay calls on one application cannot allocate twice.

OneRefPerTransition
Cardinality({rdb : r.status = "complete"}) = nextRef - MinRef

Action properties, checked as PROPERTY

Once complete or cancelled, the row (status, reference, owner) is never touched again.

TerminalStatesFrozen
□[∀ rdb : r.statusTerminalrdb]vars

The owner is the creating user and never changes; a pay/cancel by any other user leaves the database untouched.

OwnerOnly
□[ ∧ ∀ rdb : ∃ r2db : r2.id = r.idr2.owner = r.owner
∧ ∀ oOps :
(Pay(o) ∨ Cancel(o)) ⇒
rdb : r.ownero.userrdb ]vars

Applications.cfg

SPECIFICATION Spec
CONSTANTS
Users = {u1, u2}
Ids = {a, b}
Refs = {1, 2, 3}
MaxPerKind = 2
NoRef = NoRef
INVARIANTS
TypeOK
NoDuplicateCreate
RefIffComplete
UniqueRefs
OneRefPerTransition
PROPERTIES
TerminalStatesFrozen
OwnerOnly
CHECK_DEADLOCK FALSE

Generations

genchangesdistinct statesdepthpublishedraw
1 (latest) Initial model: DB as a set of rows, duplicated/interleaved single-statement requests, action properties for frozen terminal states and owner-only transitions. 9225 13 2026-08-23 05:09:36 UTC .tla .cfg

Defend this spec

Ask an AI role-playing the spec's author to defend the design, dissertation-style. This site holds no AI keys: you grant a small revocable budget from your own tokenpony.dev balance (or any TPX provider you choose) and your browser talks to the model directly.

Loading…