Why separating instructions, transaction generation, and execution creates scalable and reliable financial workflows
Scheduled financial transfers sound simple. A customer creates an instruction — for example, “transfer $100 every week” — and the system processes the transaction on the appropriate day.
Many systems begin with a straightforward implementation. A daily job reads instructions from a database, generates transactions, and attempts to process them. For a small system this can work well enough. Over time, however, it becomes difficult to reason about failures, difficult to retry safely, and increasingly difficult to scale.
When money is involved, the priority is not raw throughput or speed. The priority is correctness. A system that occasionally runs slower is tolerable. A system that moves money incorrectly is not.
This design focuses on correctness first.
Instructions vs Transactions
One of the most important architectural decisions is separating intent from execution.
Customers create instructions. Instructions represent what the customer intends to happen over time. For example, an instruction might say:
Transfer $100 every Friday.
That instruction does not move money itself. Instead, it produces transactions. A transaction represents the actual movement of money for a specific business date.
Keeping these concepts separate makes the system far easier to reason about. Instructions describe what should happen. Transactions represent what actually happens.
Evaluating Scheduled Instructions
Each business day the system evaluates which instructions should produce transactions. This evaluation must be deterministic. For any given business date the system should always be able to answer a simple question:
What transactions should exist for this day?
Bank holidays introduce a subtle complication. If a scheduled execution falls on a day when the banking system is closed, the transaction may need to be processed on the next business day. This means multiple scheduled instructions can end up executing on the same day once the bank reopens.
The system must handle this consistently so that every instruction produces the correct transaction for the correct business day.
Idempotent Transaction Generation
Transaction generation must be idempotent. If the generation process runs twice — which will inevitably happen in a real system — duplicate transactions must not be created.
A simple invariant helps enforce this behavior:
instruction_id + execution_date → exactly one transaction
If the generator attempts to create the same transaction again, the system simply recognizes that it already exists. This prevents duplicate transfers even when jobs are retried.
Once created, transactions are stored in a ledger that represents the system of record for transfers. The instruction still represents intent, while the transaction now represents an actual financial event that will be executed.
Decoupling Execution with a Queue
Rather than processing transactions directly from the database, transactions are placed onto a queue and processed asynchronously.
This small architectural decision solves several problems at once. The queue allows the system to distribute work across multiple workers, retry failed work safely, and absorb spikes in workload without overwhelming downstream systems.
Ordering still matters for financial workflows. For example, transfers for a single account should typically be processed sequentially. This can be achieved by partitioning the queue by account so that transfers for the same account remain ordered while transfers across many accounts can be processed concurrently.
Worker Processing
Workers consume transactions from the queue and execute them against the external banking system.
External financial systems are rarely perfectly reliable. Timeouts, transient errors, and rate limiting are common. Workers therefore need to respect those limits, typically through a rate limiter that controls how quickly requests are sent to the banking core.
Separating transaction generation from execution allows the system to absorb these disruptions. Workers can slow down, retry safely, or temporarily pause processing without halting the rest of the workflow.
Handling Cutoff Times
Financial transactions often must complete before a business cutoff time. Each generated transaction should therefore include a deadline, represented here as must_execute_by.
In normal cases this deadline corresponds to the scheduled execution day. If the scheduled execution falls on a bank holiday, however, the deadline may shift to the next business day when the banking system is open.
Before processing a transaction, a worker checks whether this deadline has passed. If the cutoff has been missed, the worker should not continue retrying the transaction indefinitely. At that point the transaction becomes a business exception rather than a normal processing failure.
The transaction can be marked with a state such as:
MISSED_CUTOFF
From there the system can emit the transaction into an exception workflow for any required follow-up. This might include informing the customer, alerting operations, or updating downstream reconciliation systems.
Importantly, this exception applies to the transaction itself, not the instruction that created it. Future scheduled executions should continue normally.
Reconciliation
Reconciliation is essential in financial systems because silent failures are unacceptable.
The system should periodically verify that every instruction evaluated for a business date produced the correct outcome and that every generated transaction either completed successfully, was retried, or was explicitly marked with an exception. This provides operational visibility and ensures that every scheduled instruction ultimately results in a known outcome.
Architecture Overview
+-------------------+
| Instruction Store |
| (user intent) |
+---------+---------+
|
v
+-------------------+
| Evaluation Engine |
| - due today? |
| - holiday logic |
| - idempotent gen |
+---------+---------+
|
v
+-------------------+
| Transaction Ledger|
| - execution_date |
| - must_execute_by |
| - status |
+---------+---------+
|
v
+-------------------+
| Processing Queue |
| partition/account |
+---------+---------+
|
v
+-------------------+
| Worker Pool |
| - rate limiting |
| - cutoff check |
| - call bank core |
+----+---------+----+
| |
v v
+---------+ +------------------+
| Success | | Exception Flow |
| update | | MISSED_CUTOFF |
| status | | notify, reconcile|
+---------+ +------------------+
Why This Design Works
Many systems attempt to implement scheduled transfers using a single batch job that reads instructions from the database and processes them immediately. Over time that approach tends to accumulate complexity. Retry logic becomes difficult to manage, failures halt entire batches, and scaling requires increasingly complicated coordination.
Separating the system into instruction management, transaction generation, and transaction execution results in a system that is easier to reason about and easier to operate.
Final Thoughts
Scheduled transfer systems often appear simple on the surface. In reality they are responsible for moving real money and must be designed with reliability and correctness in mind.
Separating instructions, transaction generation, and transaction execution creates a system that is far easier to operate and scale as the business grows. Transaction generation remains deterministic, execution can scale horizontally through workers, and failures are isolated rather than halting the entire system.
Real financial systems also depend on external infrastructure that is not always predictable. Banking cores impose rate limits, networks fail, and transient errors are common. Designing the workflow with queues, retries, and reconciliation allows the system to absorb those disruptions without losing work or corrupting financial state.
The key insight is that scheduled transfers are not just scheduling logic. They are financial workflows, and the architecture should treat them with the same care as any other system responsible for moving money.

I really like this transaction vs execution layer idea. This feels very similar to pysystemtrade, a futures trading git repo
I’m kind of surprised by how much manual toil comes from these systems to ensure correctness and how much visibility in terms of logging and alerting becomes important
For example, the instruction creates a transaction but the transaction in the local DB doesn’t match the external source of truth. Reconciling these states seems manual
Something else that I don’t think I saw here is that the instructions -> transaction makes it really easy to implement the transaction layer as an interface and switch out the transaction service
It would be interesting to hear more about the way that a system like this would coordinate between different timeframes, daily payments vs hourly payments. It seems like there could be conflicts there, would you move to an event based system for that?