Data lifecycle and consistency
Deletes are soft
Section titled “Deletes are soft”Deleting a record never erases it. The record is marked deleted and kept in the service’s
database, and from then on the API treats it as gone: reads return 404, lists leave it out, and
nothing can change it. A delete can’t be undone through jtWMS, in the API or in the apps.
A delete is recorded in the audit trail with the record’s last state. Where the record’s changes are published as events, the delete is too, as the record’s status-changed event with the deleted status (a cascade delete publishes one event for the whole subtree instead; see below).
Two kinds of record work differently:
- Team members are deactivated rather than deleted. They can’t sign in, stay in the team list, and can be reactivated.
- API keys are revoked. A revoked key stops working within about a minute and can’t be restored.
Active and inactive
Section titled “Active and inactive”Inactive is an operational state, not a deletion. An inactive record, such as a deactivated product or webhook subscription, is still listed, can still be read, and can be activated again. Deactivate a record you may need again; delete one you don’t.
One change, one transaction
Section titled “One change, one transaction”In the warehouse configuration, catalog and webhooks services, a write commits as one unit: the records it changes, the event it publishes and its audit entries are saved together, or none of them are. Writes that span many records are atomic too: a bulk create-or-update of up to 500 products applies every item or none, and a cascade delete removes a whole subtree or nothing.
Services don’t share databases or transactions. A change commits in the service that owns it, and where it publishes an event, webhook subscribers learn about it shortly afterwards.
Concurrent writes
Section titled “Concurrent writes”Some records are protected by optimistic concurrency. Requests don’t lock data while they work;
instead, when two overlapping writes to a protected record would conflict, the first to save wins
and the other is refused with 409 Conflict and errorCode concurrency_conflict. Nothing from
the refused request is saved and no event is published for it. The client reads the data again and
retries. The protected records are:
- storage and equipment nodes, and the writes that depend on them: creating a node under a parent, adding an equipment connection and editing a location;
- products;
- webhook subscriptions and deliveries;
- a user’s roles and a role’s permissions, where replacing the set is refused if another request removed one of its entries in the meantime.
For nodes, conflicts are detected on the records a write depends on as well as the one it changes. For example, a storage node can’t be created under a parent that another request is deleting at the same moment, and an equipment connection can’t be added to a node that is being deleted. The trade-off is that some unrelated writes to the same part of the data, such as two creates under one parent, can also conflict; a retry resolves them.
Other edits, such as to facilities, zone profiles and zones, are last write wins: two overlapping updates both succeed, and the later one is kept. The check catches requests that overlap while they run. It doesn’t catch a client that read a record earlier and writes it back later, since the API takes no version from the client.
Other 409 responses mean a request doesn’t fit the current state, such as activating a record
that is already active. Those need a different request, not a retry. See
Concurrency and deletes.
Cascade deletes
Section titled “Cascade deletes”Some records take what belongs to them when they are deleted. Deleting an equipment node deletes its configuration, its location and its connections; deleting a storage position deletes its location; deleting a product deletes its units of measure and barcodes.
A storage node with child nodes is protected: deleting it is refused with 409, listing the
children, unless the request asks for a cascade with ?cascade=true. A cascade deletes the node,
every node below it and their locations in one transaction, and publishes one event listing
everything it deleted. See Cascade deletes.