$ open blueprint
A treasury platform sits at the intersection of money movement, external banking infrastructure, financial accounting, and organizational control.
That makes it fundamentally different from a conventional CRUD application.
A payment may be initiated synchronously but confirmed asynchronously. A bank transaction may arrive independently of the application's own records. A reconciliation result may change the operational state of an account without changing the underlying financial history.
The architecture therefore has to preserve several invariants simultaneously:
- Financial records must remain immutable.
- Every monetary movement must be traceable.
- Payment submission must be idempotent.
- External events must be safely replayable.
- Tenant boundaries must never be crossed.
- Approval policies must be enforced before money moves.
- Operational state must remain distinguishable from financial truth.
This blueprint describes an architecture built around those constraints.
Executive Summary
FinCore Treasury is designed as a multi-tenant financial operations platform responsible for:
| Domain | Responsibility |
|---|---|
| Treasury | Bank accounts, balances, cash positions, liquidity |
| Banking | External bank connectivity and transaction ingestion |
| Payments | Payment creation, routing, submission, and confirmation |
| Approvals | Configurable payment authorization policies |
| Reconciliation | Matching external transactions against internal records |
| Ledger | Immutable financial journal and accounting history |
| Forecasting | Liquidity projections based on operational financial data |
| Identity | Tenant membership, roles, permissions, and access control |
| Audit | Complete history of sensitive financial operations |
The architecture follows several core principles:
- Financial state is append-only.
- External integrations are asynchronous by default.
- Every payment submission is idempotent.
- Approval policy is configuration, not hardcoded business logic.
- Operational projections are derived from authoritative records.
- Every domain operation is tenant-scoped.
- Events communicate state transitions between bounded domains.
Architectural Principles
1. Financial Truth Is Immutable
A posted financial transaction should never be edited in place.
Corrections are represented through new entries:
Original Entry
│
▼
Reversal Entry
│
▼
Corrected Entry
This makes the financial history reconstructable.
2. External Systems Are Not Authoritative Application State
Banks and payment providers are external systems.
Their APIs can:
- timeout
- retry
- return delayed confirmations
- send duplicate events
- temporarily become unavailable
- return transactions out of order
The platform therefore treats external communication as an integration boundary rather than directly mutating internal financial state.
3. Commands and Events Are Different
A command expresses intent:
SubmitPayment
ApprovePayment
ImportStatement
RequestReconciliation
An event describes something that happened:
PaymentApproved
PaymentSubmitted
PaymentConfirmed
StatementImported
TransactionMatched
JournalPosted
This distinction is fundamental to the architecture.
High-Level System Architecture
┌─────────────────────────┐
│ Client Applications │
│ Web · Mobile · API │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ API Gateway / LB │
└────────────┬────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Application Layer │
│ │
│ Auth · Tenant Context · Validation │
│ Authorization · Rate Limiting │
└─────────────────────┬──────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Treasury Domain│ │ Payment Domain │ │ Reconciliation │
│ │ │ │ │ Domain │
│ Accounts │ │ Requests │ │ Matching │
│ Cash Positions │ │ Approvals │ │ Exceptions │
│ Liquidity │ │ Submission │ │ Rules │
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
└──────────────────────────┼──────────────────────────┘
│
▼
┌─────────────────────────┐
│ Event Bus │
│ Kafka │
└────────────┬────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Ledger Domain │ │ Integration │ │ Notification / │
│ │ │ Workers │ │ Audit Consumers │
│ Journals │ │ Banks │ │ │
│ Entries │ │ Payment Rails │ │ Alerts │
│ Accounting │ │ Statements │ │ Audit Events │
└────────┬────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌──────────────────────────────┐
│ PostgreSQL │
│ │
│ Financial Truth │
│ Domain State │
│ Audit References │
└──────────────────────────────┘
┌────────────────────┐
│ Redis │
│ Cache / Locks / │
│ Short-lived State │
└────────────────────┘
Domain Architecture
The platform should be organized around business capabilities rather than technical CRUD modules.
FinCore
│
├── Identity & Access
│
├── Treasury
│ ├── Bank Accounts
│ ├── Cash Positions
│ └── Liquidity
│
├── Banking Integrations
│ ├── Bank Connections
│ ├── Statement Imports
│ └── Transaction Feeds
│
├── Payments
│ ├── Payment Requests
│ ├── Approval Policies
│ ├── Payment Submission
│ └── Confirmation
│
├── Reconciliation
│ ├── Matching
│ ├── Exceptions
│ └── Resolution
│
├── Ledger
│ ├── Journal Entries
│ ├── Journal Lines
│ └── Reversals
│
├── Forecasting
│ ├── Cash Forecast
│ ├── Inflows
│ └── Outflows
│
└── Audit
├── Activity
├── Financial Events
└── Security Events
Each domain owns its business rules and exposes operations through explicit application interfaces.
Tenant Architecture
Every financial operation belongs to an organization.
The tenant context should be established at the beginning of every authenticated request.
Request
│
▼
Authenticate User
│
▼
Resolve Organization
│
▼
Verify Membership
│
▼
Resolve Role / Permissions
│
▼
Execute Domain Operation
Every tenant-owned record should carry an organization boundary.
Organization
│
├── Bank Accounts
├── Payments
├── Approvals
├── Reconciliation Records
├── Ledger Entries
├── Forecasts
└── Audit Records
Tenant isolation must be enforced at the query and domain layers rather than relying only on frontend filtering.
Request Lifecycle
A synchronous API request should remain responsible for validating intent and creating durable state.
Client
│
▼
API Gateway
│
▼
Authentication
│
▼
Tenant Resolution
│
▼
Authorization
│
▼
Input Validation
│
▼
Domain Command
│
▼
Database Transaction
│
├── Persist State
└── Record Domain Event
│
▼
Commit
│
▼
Publish / Process Event
The API should not wait for every downstream operation to finish.
For example:
POST /payments
should create a payment request and return its durable state.
It should not block the request until a bank confirms the transfer.
Payment Domain Architecture
Payment processing is modeled as a state machine.
Draft
│
▼
Pending Approval
│
├── Rejected
│
▼
Approved
│
▼
Submitted
│
▼
Pending Confirmation
│
├── Failed
│
├── Cancelled
│
▼
Confirmed
│
▼
Ledger Posted
The important distinction is between:
Payment Intent
and:
Payment Confirmation
Creating a payment does not mean money has moved.
Payment Approval Architecture
Approval rules should be represented as data.
Payment
│
▼
Resolve Approval Policy
│
▼
Determine Required Approvers
│
▼
Create Approval Tasks
│
▼
Collect Approvals
│
▼
Policy Satisfied?
│
├── No → Remain Pending
│
└── Yes
│
▼
Submit Payment
Example:
$0 – $1,000
→ Manager
$1,000 – $25,000
→ Manager + Finance
$25,000+
→ Manager + Finance + Executive
The policy is tenant-specific.
Changing the threshold should not require a code deployment.
Payment Idempotency
Payment submission must be safe under retries.
A payment may experience:
Request
│
▼
Gateway Call
│
▼
Network Timeout
│
▼
Client Retries
The platform must distinguish:
Unknown Result
from:
Not Submitted
An idempotency key is generated when the payment instruction is created.
Payment
│
└── idempotency_key
│
▼
Payment Submission
│
▼
External Gateway
The key must remain stable across retries.
Transactional Outbox
A critical reliability problem exists when database state and event publication happen separately.
Unsafe
Database Commit
│
▼
Publish Kafka Event
If Kafka fails after the database commits, the state change exists but the event is lost.
Safer Pattern
Database Transaction
│
├── Domain State
│
└── Outbox Event
│
▼
Transaction Commit
│
▼
Outbox Publisher
│
▼
Kafka
The outbox record becomes the durable bridge between transactional state and asynchronous event delivery.
Event Architecture
The event bus decouples domains from external systems and background consumers.
Kafka
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Payment Events Bank Events Reconciliation Events
│ │ │
▼ ▼ ▼
Payment Domain Treasury Reconciliation
│ │ │
└──────────────┼──────────────┘
▼
Ledger
Example events:
PaymentCreated
PaymentApproved
PaymentSubmitted
PaymentConfirmed
PaymentFailed
BankTransactionReceived
StatementImported
TransactionMatched
ReconciliationExceptionCreated
JournalEntryPosted
JournalEntryReversed
Events should be immutable facts rather than mutable commands.
Event Processing Guarantees
Consumers should assume events can be:
- delivered more than once
- delivered late
- processed out of order
- retried
- temporarily unavailable
Therefore consumers should be idempotent.
Kafka Event
│
▼
Check Event ID
│
├── Already Processed → Ignore
│
└── New Event
│
▼
Process Event
│
▼
Record Event ID
Exactly-once behavior should not be assumed merely because Kafka is being used.
Application-level idempotency remains necessary.
Bank Integration Architecture
Bank integrations should be isolated behind provider adapters.
Banking Interface
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Bank Adapter A Bank Adapter B Bank Adapter C
│ │ │
▼ ▼ ▼
Bank API Bank API Bank API
The domain should not know provider-specific API formats.
Instead:
bank_connector.fetch_transactions()
can expose a normalized internal representation.
Provider-specific concerns remain inside the integration boundary.
Bank Transaction Ingestion
External transaction ingestion should be asynchronous.
Bank API / Feed
│
▼
Integration Worker
│
▼
Normalize Transaction
│
▼
Validate
│
▼
Deduplicate
│
▼
Persist Raw Transaction
│
▼
Publish Event
│
▼
Reconciliation Engine
Raw external data should be retained where required so that internal normalized records can always be traced back to their source.
Automated Reconciliation Architecture
Reconciliation compares two sources of truth:
External Bank Transaction
│
│
▼
Matching Engine
▲
│
│
Internal Financial Record
The matching process can progressively increase complexity.
Exact Match
│
├── Match → Reconciled
│
└── No Match
│
▼
Rule-Based Match
│
├── Match → Review / Reconcile
│
└── No Match
│
▼
Exception Queue
Potential matching dimensions:
Amount
Transaction Date
Value Date
Reference
Account
Currency
Counterparty
External Transaction ID
Reconciliation Exceptions
Not every mismatch should be automatically resolved.
Exception
│
├── Missing Ledger Entry
├── Missing Bank Transaction
├── Amount Difference
├── Currency Difference
├── Duplicate Transaction
└── Unknown Counterparty
Each exception becomes an explicit workflow rather than disappearing into an error log.
Open
│
▼
Investigating
│
├── Resolved
│
└── Escalated
This creates an auditable trail of how discrepancies were resolved.
Financial Ledger Architecture
The ledger is the financial system of record.
A journal consists of balanced lines.
Journal Entry
│
├── Debit
│ └── Accounts Payable $12,500
│
└── Credit
└── Operating Bank $12,500
The fundamental invariant is:
Total Debits = Total Credits
A posted journal should never be mutated.
Ledger Correction Model
Invalid
UPDATE journal_entries
SET amount = 11500
WHERE id = 4471;
Correct
Original Entry
│
▼
Reversal Entry
│
▼
Corrected Entry
The ledger therefore represents history rather than merely current state.
Financial State vs Operational State
One of the most important architectural distinctions is:
Financial Truth
≠
Operational Projection
For example:
Ledger
│
▼
Cash Position Projection
│
▼
Dashboard
The dashboard may be cached in Redis.
The ledger cannot be replaced by that cache.
If Redis is deleted:
Redis Lost
│
▼
Rebuild Projection
│
▼
Ledger
The system remains financially recoverable.
Cash Position Architecture
Cash position is derived from authoritative financial records and external account state.
Bank Accounts
│
├── Operating
├── Payroll
├── Reserve
└── Investment
│
▼
Transaction Stream
│
▼
Cash Position Engine
│
▼
Consolidated Position
│
▼
Redis Cache
│
▼
Dashboard
Redis is a performance layer, not the financial source of truth.
Consistency Model
Treasury systems require different consistency guarantees for different data.
| Data | Consistency Requirement |
|---|---|
| Ledger entries | Strong |
| Payment state | Strong within transaction boundaries |
| Approval state | Strong |
| Bank ingestion | Eventually consistent |
| Cash projection | Eventually consistent but reconstructable |
| Dashboard cache | Eventually consistent |
| Notifications | Asynchronous |
The architecture should therefore avoid treating every operation as either completely synchronous or completely asynchronous.
Consistency should be chosen according to the business invariant.
Database Architecture
PostgreSQL acts as the primary transactional store.
Core entities include:
organizations
users
organization_memberships
bank_connections
bank_accounts
bank_transactions
payments
payment_submissions
payment_approvals
reconciliation_records
reconciliation_exceptions
journal_entries
journal_lines
audit_events
outbox_events
Financial tables should favor explicit relationships and immutable historical records over denormalized mutable totals.
Database Transaction Boundaries
Operations that modify related financial state should execute inside explicit transactions.
Example:
Approve Payment
│
▼
BEGIN
│
├── Validate Approval
├── Record Approval
├── Update Payment State
└── Create Outbox Event
│
▼
COMMIT
The event should not be published until the database transaction has successfully committed.
Concurrency Control
Financial systems must assume concurrent operations.
Potential race:
User A ──┐
├── Approve Payment
User B ──┘
Without concurrency protection, both requests may believe they are the final approval.
The system should use appropriate transactional controls:
Database Transaction
│
▼
Row-Level Lock / Version Check
│
▼
Validate Current State
│
▼
Apply Transition
State transitions should be conditional on the expected current state.
State Machines Over Boolean Flags
Avoid modeling complex financial workflows with unrelated booleans.
Fragile
approved = true
submitted = true
failed = false
confirmed = true
These fields can produce contradictory combinations.
Better
payment.status = CONFIRMED
with explicit allowed transitions:
DRAFT
↓
PENDING_APPROVAL
↓
APPROVED
↓
SUBMITTED
↓
PENDING_CONFIRMATION
↓
CONFIRMED
State transitions become domain rules rather than accidental combinations of flags.
Audit Architecture
Financial systems require more than application logs.
Audit records should capture:
Who
What
When
Organization
Resource
Previous State
New State
Reason
Request ID
IP / Client Context
Example:
{
"actor": "user_42",
"action": "payment.approved",
"resource": "payment_9182",
"organization": "org_17",
"request_id": "req_abc123"
}
Audit records should be append-only.
Security Architecture
Sensitive financial operations should pass through multiple control boundaries.
Authentication
│
▼
Tenant Isolation
│
▼
Authorization
│
▼
Approval Policy
│
▼
Idempotency
│
▼
Transaction Boundary
│
▼
External Submission
Important controls include:
- Strong authentication
- Tenant isolation
- Role and permission enforcement
- Dual-control approval
- Idempotent payment submission
- Secret management
- Encryption in transit
- Encryption at rest
- Audit logging
- Rate limiting
- Provider signature verification
Background Processing
Celery is appropriate for workloads that do not need to block API requests.
Examples:
Statement Import
│
▼
Celery Worker
│
├── Parse
├── Normalize
├── Deduplicate
└── Publish Event
Other candidates:
- Scheduled bank synchronization
- Reconciliation jobs
- Forecast generation
- Notifications
- Report generation
- Retry processing
Kafka remains the event backbone, while Celery handles task-oriented background execution.
Failure Handling
External failures are expected.
Example:
Payment Submitted
│
▼
Bank Timeout
│
▼
Payment State = UNKNOWN / PENDING
│
▼
Retry / Status Inquiry
│
▼
Confirmation Event
The system should never interpret a timeout as proof that the payment failed.
This distinction is critical:
Request Failed
≠
Payment Failed
The external system's authoritative confirmation determines the final financial state.
Observability
Production treasury infrastructure requires visibility across synchronous and asynchronous paths.
Metrics
Payment Submission Rate
Payment Failure Rate
Payment Confirmation Latency
Reconciliation Match Rate
Reconciliation Exception Rate
Bank Feed Delay
Kafka Consumer Lag
Celery Queue Depth
Database Latency
API Error Rate
Structured Logging
{
"request_id": "req_123",
"organization_id": "org_42",
"payment_id": "pay_9182",
"event": "payment.confirmed"
}
Distributed Tracing
A request should remain traceable across:
API
↓
Database
↓
Outbox
↓
Kafka
↓
Consumer
↓
External Provider
Common Architectural Mistakes
Updating the Ledger Before Payment Confirmation
Bad:
Submit Payment
↓
Immediately Post Ledger
Better:
Submit Payment
↓
Await Confirmation
↓
Post Financial Entry
Using Redis as Financial Truth
Bad:
Redis Cash Balance
↓
Financial Report
Better:
Ledger / Bank Records
↓
Derived Cash Projection
↓
Redis
↓
Dashboard
Hardcoding Approval Rules
Bad:
if amount > 25000:
require_executive()
Better:
Tenant
↓
Approval Policy
↓
Threshold Rules
↓
Approval Chain
Treating Kafka as Exactly-Once Magic
Bad assumption:
Kafka = exactly once
Better:
At-least-once delivery
+
Idempotent consumers
+
Event IDs
+
Transactional boundaries
Mutable Financial Records
Bad:
UPDATE journal_entry
Better:
Reversal
+
Corrective Entry
Evolution Path
The platform can evolve without changing its core financial invariants.
Centralized Bank Records
│
▼
Bank Transaction Ingestion
│
▼
Payment Requests
│
▼
Approval Governance
│
▼
Payment Orchestration
│
▼
Automated Reconciliation
│
▼
Immutable Financial Ledger
│
▼
Event-Driven Operations
│
▼
Cash Forecasting
│
▼
Advanced Liquidity Management
The important architectural decision is establishing the financial and event boundaries early.
Additional capabilities can then be built around those foundations.
Reference Implementation Stack
A production implementation could use:
Application
Django
Django REST Framework
Database
PostgreSQL
Event Backbone
Kafka
Caching
Redis
Background Processing
Celery
Authentication
JWT / OAuth depending on client type
Documentation
OpenAPI
Infrastructure
Docker
Observability
Structured Logs
Metrics
Distributed Tracing
The technology choices are replaceable.
The architectural invariants are not.
What I Would Avoid Initially
Microservices Everywhere
A modular monolith can provide strong domain boundaries without introducing distributed-system complexity prematurely.
Synchronous External Payment Chains
External banks should not determine API response latency.
Mutable Financial State
Avoid storing financial truth as editable totals.
Hardcoded Governance
Approval rules should be tenant-configurable.
Cache-Dependent Accounting
Caches should accelerate financial projections, never become the source of financial truth.
Premature Event Proliferation
Not every internal function needs a Kafka event.
Events should represent meaningful domain facts or integration boundaries.
Architectural Invariants
The system should continuously preserve these rules:
1. Every financial operation belongs to a tenant.
2. Every posted journal is balanced.
3. Posted financial entries are immutable.
4. Every payment submission is idempotent.
5. Approval must precede payment execution.
6. External confirmation determines final payment state.
7. Derived projections can be rebuilt from authoritative data.
8. Events are treated as replayable facts.
9. Consumers are idempotent.
10. Sensitive operations are auditable.
These invariants are more important than any individual framework or infrastructure component.
Final Architecture Perspective
FinCore Treasury is not fundamentally a banking dashboard.
It is a distributed financial system operating across two worlds:
Internal Financial State
│
│
▼
FinCore Platform
│
│
▼
External Financial Infrastructure
The architecture exists to maintain correctness across that boundary.
Banks control when external events happen.
Payment providers control when transactions are confirmed.
Users control when payment requests are created.
Finance teams control approval policies.
The platform must coordinate all of them without losing financial integrity.
That leads to the core architectural model:
Commands
│
▼
Validated Domain State
│
▼
Transactional Persistence
│
▼
Durable Events
│
▼
Asynchronous Consumers
│
├── External Integrations
├── Reconciliation
├── Ledger
├── Cash Projections
└── Audit
The result is a system where financial truth remains immutable, external operations remain asynchronous, failures remain recoverable, and every important monetary operation can be reconstructed from its history.


