Skip to main content

Transfer

A transfer is an immutable record of a financial transaction between two accounts.

In TigerBeetle, financial transactions are called "transfers" instead of "transactions" because the latter term is heavily overloaded in the context of databases.

Note that transfers debit a single account and credit a single account on the same ledger. You can compose these into more complex transactions using the methods described in Currency Exchange and Multi-Debit, Multi-Credit Transfers.

Updates

Transfers cannot be modified after creation.

Guarantees

  • Transfers are immutable. They are never modified once they are successfully created.
  • There is at most one Transfer with a particular id.
  • A pending transfer resolves at most once.
  • Transfer timeouts are deterministic, driven by the cluster's timestamp.

Modes

Transfers can either be Single-Phase, where they are executed immediately, or Two-Phase, where they are first put in a Pending state and then either Posted or Voided. For more details on the latter, see the Two-Phase Transfer guide.

Fields used by each mode of transfer:

FieldSingle-PhasePendingPost-PendingVoid-Pending
idrequiredrequiredrequiredrequired
debit_account_idrequiredrequiredoptionaloptional
credit_account_idrequiredrequiredoptionaloptional
amountrequiredrequiredoptionaloptional
pending_idnonenonerequiredrequired
user_data_128optionaloptionaloptionaloptional
user_data_64optionaloptionaloptionaloptional
user_data_32optionaloptionaloptionaloptional
timeoutnoneoptionalnonenone
ledgerrequiredrequiredoptionaloptional
coderequiredrequiredoptionaloptional
flags.linkedoptionaloptionaloptionaloptional
flags.pendingfalsetruefalsefalse
flags.post_pending_transferfalsefalsetruefalse
flags.void_pending_transferfalsefalsefalsetrue
flags.balancing_debitoptionaloptionalfalsefalse
flags.balancing_creditoptionaloptionalfalsefalse
timestampnonenonenonenone

Fields

id

This is a unique identifier for the transaction.

Constraints:

  • Type is 128-bit unsigned integer (16 bytes)
  • Must not be zero or 2^128 - 1
  • Must not conflict with another transfer in the cluster

See the id section in the data modeling doc for more recommendations on choosing an ID scheme.

Note that transfer IDs are unique for the cluster -- not the ledger. If you want to store a relationship between multiple transfers, such as indicating that multiple transfers on different ledgers were part of a single transaction, you should store a transaction ID in one of the user_data fields.

debit_account_id

This refers to the account to debit the transfer's amount.

Constraints:

  • Type is 128-bit unsigned integer (16 bytes)
  • When flags.post_pending_transfer and flags.void_pending_transfer are unset:
    • Must match an existing account
    • Must not be the same as credit_account_id
  • When flags.post_pending_transfer or flags.void_pending_transfer are set:
    • If debit_account_id is zero, it will be automatically set to the pending transfer's debit_account_id.
    • If debit_account_id is nonzero, it must match the corresponding pending transfer's debit_account_id.

credit_account_id

This refers to the account to credit the transfer's amount.

Constraints:

  • Type is 128-bit unsigned integer (16 bytes)
  • When flags.post_pending_transfer and flags.void_pending_transfer are unset:
    • Must match an existing account
    • Must not be the same as debit_account_id
  • When flags.post_pending_transfer or flags.void_pending_transfer are set:
    • If credit_account_id is zero, it will be automatically set to the pending transfer's credit_account_id.
    • If credit_account_id is nonzero, it must match the corresponding pending transfer's credit_account_id.

amount

This is how much should be debited from the debit_account_id account and credited to the credit_account_id account.

Note that this is an unsigned 128-bit integer. You can read more about using debits and credits to represent positive and negative balances as well as fractional amounts and asset scales.

  • When flags.balancing_debit is set, this is the maximum amount that will be debited/credited, where the actual transfer amount is determined by the debit account's constraints.
  • When flags.balancing_credit is set, this is the maximum amount that will be debited/credited, where the actual transfer amount is determined by the credit account's constraints.

Constraints:

  • Type is 128-bit unsigned integer (16 bytes)
  • When flags.post_pending_transfer is set:
    • If amount is zero, it will be automatically be set to the pending transfer's amount.
    • If amount is nonzero, it must be less than or equal to the pending transfer's amount.
  • When flags.void_pending_transfer is set:
    • If amount is zero, it will be automatically be set to the pending transfer's amount.
    • If amount is nonzero, it must be equal to the pending transfer's amount.
  • When flags.balancing_debit and/or flags.balancing_credit is set, if amount is zero, it will automatically be set to the maximum amount that does not violate the corresponding account limits. (Equivalent to setting amount = 2^128 - 1).
  • When all of the following flags are not set, amount must be nonzero:
    • flags.post_pending_transfer
    • flags.void_pending_transfer
    • flags.balancing_debit
    • flags.balancing_credit

Examples

pending_id

If this transfer will post or void a pending transfer, pending_id references that pending transfer. If this is not a post or void transfer, it must be zero.

See the section on Two-Phase Transfers for more information on how the pending_id is used.

Constraints:

  • Type is 128-bit unsigned integer (16 bytes)
  • Must be zero if neither void nor pending transfer flag is set
  • Must match an existing transfer's id if non-zero

user_data_128

This is an optional 128-bit secondary identifier to link this transfer to an external entity or event.

As an example, you might generate a TigerBeetle Time-Based Identifier that ties together a group of transfers.

For more information, see Data Modeling.

Constraints:

  • Type is 128-bit unsigned integer (16 bytes)

user_data_64

This is an optional 64-bit secondary identifier to link this transfer to an external entity or event.

As an example, you might use this field store an external timestamp.

For more information, see Data Modeling.

Constraints:

  • Type is 64-bit unsigned integer (8 bytes)

user_data_32

This is an optional 32-bit secondary identifier to link this transfer to an external entity or event.

As an example, you might use this field to store a timezone or locale.

For more information, see Data Modeling.

Constraints:

  • Type is 32-bit unsigned integer (4 bytes)

timeout

This is the interval in seconds after a pending transfer's arrival at the cluster that it may be posted or voided. Zero denotes absence of timeout.

Non-pending transfers cannot have a timeout.

TigerBeetle makes a best-effort approach to remove pending balances of expired transfers automatically:

  • Transfers expire exactly at their expiry time (timestamp plus timeout converted in nanoseconds).

  • Pending balances will never be removed before its expiry.

  • Expired transfers cannot be manually posted or voided.

  • It is not guaranteed that the pending balance will be removed exactly at its expiry.

    In particular, client requests may observe still-pending balances for expired transfers.

  • Pending balances are removed in chronological order by expiry. If multiple transfers expire at the same time, then ordered by the transfer's creation timestamp.

    If a transfer A has expiry E₁ and transfer B has expiry E₂, and E₁<E₂, if transfer B had the pending balance removed, then transfer A had the pending balance removed as well.

Constraints:

  • Type is 32-bit unsigned integer (4 bytes)
  • Must be zero if flags.pending is not set

The timeout is an interval in seconds rather than an absolute timestamp because this is more robust to clock skew between the cluster and the application. (Watch this talk on Detecting Clock Sync Failure in Highly Available Systems on YouTube for more details.)

ledger

This is an identifier that partitions the sets of accounts that can transact with each other.

See data modeling for more details about how to think about setting up your ledgers.

Constraints:

  • Type is 32-bit unsigned integer (4 bytes)
  • When flags.post_pending_transfer or flags.void_pending_transfer is set:
    • If ledger is zero, it will be automatically be set to the pending transfer's ledger.
    • If ledger is nonzero, it must match the ledger value on the pending transfer's debit_account_id and credit_account_id.
  • When flags.post_pending_transfer and flags.void_pending_transfer are not set:
    • ledger must not be zero.
    • ledger must match the ledger value on the accounts referenced in debit_account_id and credit_account_id.

code

This is a user-defined enum denoting the reason for (or category of) the transfer.

Constraints:

  • Type is 16-bit unsigned integer (2 bytes)
  • When flags.post_pending_transfer or flags.void_pending_transfer is set:
    • If code is zero, it will be automatically be set to the pending transfer's code.
    • If code is nonzero, it must match the pending transfer's code.
  • When flags.post_pending_transfer and flags.void_pending_transfer are not set, code must not be zero.

flags

This specifies (optional) transfer behavior.

Constraints:

flags.linked

This flag links the result of this transfer to the outcome of the next transfer in the request such that they will either succeed or fail together.

The last transfer in a chain of linked transfers does not have this flag set.

You can read more about linked events.

Examples

flags.pending

Mark the transfer as a pending transfer.

flags.post_pending_transfer

Mark the transfer as a post-pending transfer.

flags.void_pending_transfer

Mark the transfer as a void-pending transfer.

flags.balancing_debit

Transfer at most amount — automatically transferring less than amount as necessary such that debit_account.debits_pending + debit_account.debits_posted ≤ debit_account.credits_posted. If amount is set to 0, transfer at most 2^64 - 1 (i.e. as much as possible).

If the highest amount transferable is 0, returns exceeds_credits.

Retrying a balancing transfer will return exists_with_different_amount if the amount of the retry differs from the amount that was actually transferred.

The amount of the recorded transfer is set to the actual amount that was transferred, which is less than or equal to the amount that was passed to create_transfers.

flags.balancing_debit is exclusive with the flags.post_pending_transfer/flags.void_pending_transfer flags because posting or voiding a pending transfer will never exceed/overflow either account's limits.

flags.balancing_debit is compatible with (and orthogonal to) flags.balancing_credit.

Examples

flags.balancing_credit

Transfer at most amount — automatically transferring less than amount as necessary such that credit_account.credits_pending + credit_account.credits_posted ≤ credit_account.debits_posted. If amount is set to 0, transfer at most 2^64 - 1 (i.e. as much as possible).

If the highest amount transferable is 0, returns exceeds_debits.

Retrying a balancing transfer will return exists_with_different_amount if the amount of the retry differs from the amount that was actually transferred.

The amount of the recorded transfer is set to the actual amount that was transferred, which is less than or equal to the amount that was passed to create_transfers.

flags.balancing_credit is exclusive with the flags.post_pending_transfer/flags.void_pending_transfer flags because posting or voiding a pending transfer will never exceed/overflow either account's limits.

flags.balancing_credit is compatible with (and orthogonal to) flags.balancing_debit.

Examples

timestamp

This is the time the transfer was created, as nanoseconds since UNIX epoch.

It is set by TigerBeetle to the moment the transfer arrives at the cluster.

You can read more about Time in TigerBeetle.

Constraints:

  • Type is 64-bit unsigned integer (8 bytes)
  • Must be set to 0 by the user when the Transfer is created

Internals

If you're curious and want to learn more, you can find the source code for this struct in src/tigerbeetle.zig. Search for const Transfer = extern struct {.

You can find the source code for creating a transfer in src/state_machine.zig. Search for fn create_transfer(.