A command cancels an order, creates its replacement and then checks the account's open-order limit. Each step reads state that the same command has just changed. Looking only at the stored records would count the cancelled order and miss the replacement. Updating the records immediately would make a later business rejection difficult to undo.

StateVec handles this through a stacked data view: a transaction reads its own changes over the accepted changes that precede it, with the node's applied state underneath. Record reads, unique-key lookups and canonical indexes all use this view. The handler can make a complete business decision while its effects remain provisional.

The important property is agreement between those access paths. If a record now says Cancelled, an index over open orders must stop counting it. If a new order is visible by its record ID, its unique key must resolve to that same record. A rejection must remove all of the command's effects together.

The state a command is allowed to see

The base is committed state already applied by the node, also called canonical state. Simulation adds layers above it without modifying that base.

Layer, newest firstWhat it containsWhen it becomes visible
Current commandIts provisional record writes, key changes and eventsDuring that command's execution
Current batchAccepted effects from earlier commands in the batchAfter each earlier command succeeds
Earlier prepared groups, when presentAn ordered prefix that continued simulation depends onWhen that prefix is supplied as the simulation base
Applied baseRecords and access paths already installed on the nodeBefore the new simulation begins

Here, accepted means that a command passed its business rules. Cluster commitment is a separate step. An accepted command may supply state to later simulation while its changes are still absent from canonical state.

Reads search from the current command toward the applied base. A newer value wins; an explicit deletion stops the search. Earlier prepared groups are present when simulation continues over an existing prefix.
Reads search from the current command toward the applied base. A newer value wins; an explicit deletion stops the search. Earlier prepared groups are present when simulation continues over an existing prefix.

In the implementation, TxAccess owns the current command's writes. BatchOverlayView exposes the accepted changes in the current batch, and StackedBatchOverlayView can read through earlier prepared groups. These views compose over a borrowed base; they do not require a complete copy of the business state for each command.

Later commands are outside the view. Command 12 can see accepted effects from command 11, but it cannot see command 13. Every replica must reconstruct that same ordered prefix before evaluating command 12.

A record lookup has three answers at each layer

A layer must distinguish no entry, a value, and a deletion.

What the layer saysWhat the read does
No entry for this recordContinue to the next older layer
Created or updated recordReturn this version and stop
Deleted recordReturn missing and stop

A deletion is often called a tombstone. It is an explicit instruction to hide the older record. Treating it as a cache miss would make a deleted record reappear from the base.

For example, suppose the applied base contains account 7 with a balance of 100. An earlier accepted command in the batch changes it to 125. The current command stages a balance of 110. A read in that command returns 110. If the command is refused, its layer is discarded and the next command sees 125. If it stages a deletion instead, its own read returns missing even though older layers still contain the account.

The same precedence applies whether the handler reads a record repeatedly or reaches it through another access path. A lookup must not mix a newer identity mapping with stale record bytes.

Unique keys resolve identity through the same stack

A unique key, or UK, maps a business identity to an engine record ID. An order might use (account_id, client_order_id). The record kind and UK identifier select the appropriate mapping; the key's encoded fields identify one live record within it.

The engine stages these mappings alongside the record changes:

OperationRecord viewUnique-key view
Create an orderNew record is readableIts key resolves to the new record
Update quantity or statusUpdated record is readableThe existing identity still resolves to it
Delete the orderRecord is hiddenIts key is explicitly removed from the visible mapping
Refuse the commandIts provisional changes disappearIts provisional mappings disappear with them

A unique-key lookup searches newer mappings first, then resolves the resulting record ID through the layered record view. Both steps matter. Finding the right ID is insufficient if the record read then bypasses a staged update or deletion.

Unique-key fields are immutable in the current schema contract. Changing an order's status updates its record and relevant indexes; it does not rewrite the order's identity. A cancelled order that remains stored therefore still occupies its unique key. Applications can use that retained identity to recognize duplicate requests according to their own business rules.

Uniqueness checks include the current command and the accepted prefix below it. Two creates cannot claim the same live identity simply because neither record has reached canonical state yet. Handlers should detect an expected duplicate and return a business refusal; an unchecked uniqueness violation is a host error.

An index update changes membership and order

A canonical index is a schema-defined access path used during execution. For an index over (account_id, status), cancelling an order changes where it belongs even though its unique key stays the same.

The engine derives index effects from the record's earlier and current values:

  • A create adds an entry for the new indexed fields.
  • A delete removes the entry for the earlier fields.
  • An update removes the old entry and adds the new entry when the indexed key changes.
  • A change to an unrelated field leaves index membership unchanged.

Entries are ordered by encoded index key, then record ID. The record ID supplies a deterministic tie-breaker when several records share the same indexed fields. Appending staged records in insertion order would not preserve that ordering.

A visible index is built by filtering out removed entries and merging in additions. With several layers, the effects are composed in order so that newer changes can replace earlier ones. This lets a record move between prefixes more than once during a batch without leaving duplicate or obsolete entries behind.

For one open-order prefix, removing records 41 and 42 and adding record 43 changes the ordered result from [41, 42, 45] to [43, 45]. The removals hide older entries even though the base is unchanged.
For one open-order prefix, removing records 41 and 42 and adding record 43 changes the ordered result from [41, 42, 45] to [43, 45]. The removals hide older entries even though the base is unchanged.

Limits apply to the visible result

Suppose a query needs the first two visible entries in that example. Taking only the first two base entries, 41 and 42, would be wrong: both are removed. Adding 43 would produce a single result and miss 45.

The engine must inspect enough older candidates to account for removals, then apply the output limit to the merged result. The implementation expands the lower-layer scan budget by the relevant removal count. Across stacked groups, it carries that requirement down toward the base and merges results back upward.

This also matters for capped counts. A cancelled order must not consume an open-order slot, and a newly accepted order must consume one immediately for the next command. Counting only the base, or adding staged inserts without subtracting removals, can accept too many orders or refuse a valid one.

A batch, command by command

Consider account 7 with two applied open orders, 41 and 42. Three commands run in one simulation batch:

CommandView during executionOutcomeOpen orders visible to the next command
Cancel order 41Order 41 becomes Cancelled; it leaves the Open index prefixAccepted42
Create order 43Its record, unique key and Open index entry are addedAccepted42, 43
Create order 44, then fail a risk checkThe command can temporarily read its new record and index entryRefused42, 43

The third command may have staged several writes before its final check fails. Finalization discards its record changes, unique-key mutations and provisional events. Its index effects disappear with those writes. The accepted effects of commands 1 and 2 remain available to the rest of the batch.

The applied base is still unchanged during simulation: it contains the original open orders 41 and 42. The batch overlay contains the candidate cancellation of 41 and creation of 43. No part of order 44 may remain in that candidate.

A refusal still has an outcome and a position in transaction history. Batching does not turn these commands into one business transaction whose rejection rolls back all its neighbors. Each command keeps its own success or refusal, even when their accepted changes are installed together later.

The batch can retain the latest staged version of a repeatedly changed record instead of copying every intermediate version into the final installation. That optimization must preserve each command's outcome and execution evidence.

Use bounded index questions in business logic

The typed plugin API exposes canonical indexes through count_index_prefix_capped. It answers a bounded business question such as “has this account reached its open-order limit?” The ordered scans discussed above explain how the engine constructs that answer internally.

For a generated Order index named by_account_status over (account_id, status), a handler can check a positive limit before creating another order. This fragment uses a u8 status value named OPEN and an application error type that converts host errors:

Rust
use statevec::CanonicalIndexCount;

let open_orders = tx.count_index_prefix_capped::<Order, _>(
    Order::BY_ACCOUNT_STATUS_INDEX_ID,
    Order::index_by_account_status(&account_id, &OPEN),
    limit,
)?;

if matches!(open_orders, CanonicalIndexCount::AtLeast(_)) {
    return Err(OrderError::OpenOrderLimit);
}

Exact(n) means fewer than limit entries exist and the count is known. AtLeast(limit) means the threshold has been reached; it does not claim that the total is exactly limit. A handler enforcing a limit has enough information to decide without asking for a complete result set.

Composite-key order determines which prefixes are useful. With (account_id, status), the account prefix covers all orders for that account, while the full key selects one status within it. A status-only prefix does not select that status across every account. The generated key and prefix helpers use the schema's encoding, so handlers should use them consistently.

The current schema allows up to three fields in a canonical index. Choose them around bounded decisions the handler actually makes. Broader reporting and search belong in downstream views.

Simulation prepares a result; apply installs it

In Verify mode, the leader simulates commands against the layered view and followers repeat the execution from the corresponding starting state. The resulting changes and execution evidence can be compared before replication is acknowledged. The stack makes those candidate results available without installing them as live business state.

After commitment, a node may reuse its own prepared result when the starting state, inputs and required execution evidence match exactly. Otherwise it re-executes and checks. An overlay belongs to a particular base and ordered prefix; a cached result cannot be attached to a different base merely because the final command bytes look familiar.

In Performance mode, business execution begins at committed apply. The transaction and batch views still give handlers read-your-writes behavior and keep each business refusal from leaking partial changes. The difference is when execution occurs relative to commitment.

A panic or an execution failure has a separate path from a business refusal. The engine cannot continue a simulation whose result is undetermined as if it were an ordinary rejected order. An execution fault while applying committed work stops the node; recovery reconstructs state from the selected checkpoint and committed log.

What this design saves, and what it costs

The layered view avoids copying the complete state for every command. Unchanged records remain in the base; overlays hold changed records, key mappings, index additions and removals, and provisional results. Discarding a refused command removes its provisional work instead of requiring compensating writes to the live store.

That work still has a cost. Modified record bytes consume memory. Key lookups may search several layers. Index merges must account for hidden entries, and a prefix with many staged removals may require reading more base candidates than the result cap alone suggests. Retaining more prepared groups also retains more pending state.

Short handlers, bounded batches and appropriately capped queries keep those costs manageable. The engine can reuse scratch allocations and maintain accumulated index effects, but each optimization must preserve the same visible records, identities and ordered results.

The practical contract for a handler is precise: read its own writes, include the accepted commands before it, exclude later commands, and make every access path agree. Those are the conditions that let the same business logic run during simulation, verification and recovery and reach the same decision.

Further reading