Digital Twin · The balance model
Start here

The balance model

Every transaction code posts to one or more balances. Read across its row: each cell tells you what happens to that balance — it adds money (+$) or it takes money (−$). That’s the whole idea.

The matrix

Rows are transaction codes. Columns are balances. Cells are what the code does to each balance.

Code ╲ BalanceAvailable
money
Held
money
Pix used
today
Cash deposit+$
Cash withdrawal−$
Card hold authorization−$+$
Pix sent−$+$
Pix received+$
+$ adds the amount −$ takes the amount doesn’t touch it
Read the Card hold row: it takes (−$) the customer’s Available money and adds (+$) the same amount into Held — one code, moving money from one balance to another.

What a cell can do

Two options. That’s the entire vocabulary for now.

+$ — the balance goes up by the amount.  −$ — the balance goes down by the amount.

A code is a credit or a debit — but each balance can be set either way. That’s how a single code can add to one balance and take from another (the card hold above), instead of only ever doing one thing.

The one thing that trips everyone: “a limit is a balance”. Don’t overthink it — here, a limit is just another balance. The “Pix used today” column is a limit; it moves with the same +$ / −$ rules as every other balance, it simply has a ceiling. Same machine, different balance.

Level 2 — when one account type differs

Sometimes a code must touch different balances for a specific account type. The naïve idea is “one full matrix per type” — with dozens of account types that’s dozens of copies to keep in sync. It’s so impractical that there is no per-type matrix at all. Instead:

Keep one base matrix for everybody (the one above), then list only the cells that differ for specific account types. Everything not listed uses the base.

Exceptions · only these account types differ
Account typeCodeInstead of the base…
SavingsPix sentroute “used today” to the Savings pix limit balance instead
PayrollPix sentno limit balance — only Available −$

Every account type not in this list just uses the base. A short exceptions list, never a full copy per type.

The one rule when you configure it: an exception must sit next to its base as a higher-priority variant of the same code’s rule, so the specific one wins and the base is skipped for those types. Added as a separate rule, it stacks (both apply) instead of replacing. Same rule, exception on top = override.

Sending the same transaction twice is safe

Every transaction you send carries a correlationId — your own reference for it. If you send the same correlationId again (say a timeout made you retry and you’re not sure the first one landed), Digital Twin recognizes it and does not post it a second time — it’s treated as a replay, not a new transaction.

So the rule is simple: give each transaction a stable, unique correlationId, and reuse the same one if you retry. It’s the same field whether you send over REST or Kafka, and it travels inside the message (not an HTTP header) — so retries are safe with no special replay handling on your side.
← Ledger model Lightweight rule engine →