A multi-tenant banking platform: core banking with double-entry transactions and interest accrual, UK Faster Payments, and tenant onboarding with IDV.
Queenswood.Banking.-.27.March.2026.mp4
| Capability | Description |
|---|---|
| Payments & Transactions | Internal transfers, outbound UK Faster Payments via a pluggable scheme adapter, inbound settlement with BBAN lookup and idempotency |
| Interest | Daily accrual and monthly capitalisation with fractional carry at sub-minor-unit precision |
| Cash Accounts | Open accounts against published products, assigned UK SCAN payment addresses (sort code + account number). Lifecycle: opening → opened → closing → closed |
| Cash Account Products | Draft products with balance configurations, publish versioned releases |
| Parties & Identity | Register customers with national identifiers; Onfido-shaped IDV via pluggable adapter drives pending → active (or rejected) |
| Organisations & API Keys | Multi-tenant onboarding — create a tenant, issue API keys (returned once, stored hashed) |
API documentation: kjothen.github.io/queenswood | OpenAPI at localhost:8080 when running.
The engineering choices that shape this codebase, each linked to the doc that goes deep on it:
- One unified API for the whole bank, with full OpenAPI 3.x compliance. Bank-shaped, not implementation-shaped; the spec is the contract. See ADR-0013 and ADR-0014.
- Policies and bindings are first-class data, not hardcoded rules. Capabilities and limits as records; a curative-permit pattern that lets a customer self-correct out of breach. See policy-evaluation.
- Daily interest accrual that conserves pennies. Integer micro-unit arithmetic with sub-minor-unit carry; six-leg postings at capitalisation; cadence (daily, monthly, anything) is the operator's choice. See interest.
- A pure-functional model runs alongside the real system; tests pass only when they agree. Property-based testing via fugato + hand-authored EDN scenarios share one runner. See scenario-testing.
- Anomalies, not exceptions, at every component interface. Three semantic kinds (error / rejection / unauthorized) mapping directly to HTTP status families. See ADR-0005.
- System-as-data via donut.system + YAML. Components are records, profiles are values, testcontainers and production share one bootstrap path. See ADR-0007 and the slides.
- FoundationDB Record Layer with the changelog as the transactional outbox. Multi-record ACID by default; the outbox pattern falls out of the storage engine. See ADR-0002.
- Domain fork of
mono— infrastructure bricks present in the workspace, not pulled in as a library. See ADR-0001.
Per-domain deployable services on two substrates — Apache Pulsar for command and event flow, FoundationDB Record Layer for storage and changelog. Adapter/simulator pairs front the two external integrations (UK Faster Payments via ClearBank, IDV via Onfido); the simulators stand in for the production providers in development and tests.
graph TB
APP["bank-app<br/>(Svelte UI)"]
subgraph http ["HTTP services"]
direction LR
API[bank-api-service]
CBA[bank-clearbank-<br/>adapter-service]
CBS[bank-clearbank-<br/>simulator-service]
OFA[bank-onfido-<br/>adapter-service]
OFS[bank-onfido-<br/>simulator-service]
end
PULSAR[("Apache Pulsar<br/>command + event topics")]
subgraph processors ["Processor services (Pulsar consumers)"]
direction LR
PCA[cash-account-<br/>processor]
PPT[party-<br/>processor]
PPY[payment-<br/>processor]
PIN[interest-<br/>processor]
PTX[transaction-<br/>processor]
PID[idv-<br/>processor]
end
FDB[("FoundationDB<br/>Record Layer + changelog")]
subgraph oneshots ["Cold-start (one-shot k8s Jobs)"]
direction LR
MIG[migrator-service]
BS[bootstrap-service]
end
subgraph external ["External (production targets)"]
direction LR
CB[ClearBank FPS]
OF[Onfido]
end
APP -->|HTTP| API
API -->|commands| PULSAR
API -->|direct CRUD<br/>org, api-key, product, policy| FDB
PULSAR -->|consume commands| processors
processors -->|read + write| FDB
FDB -->|changelog| processors
PPY -->|submit-payment| PULSAR
PULSAR -->|consume| CBA
CBA <-->|HTTP + webhook| CBS
CBA <-.->|HTTP + webhook| CB
CBA -->|transaction-settled| PULSAR
PID -->|submit-idv-check| PULSAR
PULSAR -->|consume| OFA
OFA <-->|HTTP + webhook| OFS
OFA <-.->|HTTP + webhook| OF
OFA -->|idv-completed| PULSAR
MIG -->|FDB metadata| FDB
MIG -->|topics + schemas| PULSAR
MIG --> BS
BS -->|internal org,<br/>platform policies| FDB
HTTP services — bank-api-service is the public banking
surface (Reitit + Malli + Sieppari + Muuntaja). The
adapter/simulator pairs serve their own HTTP surfaces:
adapters host webhook receivers and call out to providers;
simulators stand in for the providers in development and
tests.
Direct path — low-volume, idempotent records
(organisations, products, policies, API keys) are created
and updated directly by bank-api-service against FDB. All
records query on-demand using FDB record primary key
ordering.
Commands path — high-volume activity (parties, cash
accounts, payments, interest, transactions) flows as
Avro-serialised commands from bank-api-service through
Pulsar to a domain processor. Each processor writes to FDB
and replies via the same bus. Envelope statuses: ACCEPTED
(2xx), REJECTED (4xx), FAILED (5xx). See
transaction-processing.
Scheme + IDV paths — outbound payments publish a
submit-payment command on a scheme channel;
bank-clearbank-adapter-service consumes, calls FPS, and
republishes settlement webhooks as transaction-settled
events. The IDV path mirrors this:
bank-idv-processor-service publishes submit-idv-check,
bank-onfido-adapter-service calls Onfido, the
check.completed webhook becomes an idv-completed event.
The simulator services stand in for ClearBank FPS and
Onfido respectively; the dotted edges to ClearBank and
Onfido mark the production targets.
Watchers — FDB changelog triggers drive reactive
state transitions inside the processor services: cash
account opening → opened and closing → closed;
the party–IDV–party activation chain (party-processor
writes a pending party, idv-processor reacts to the
party changelog and initiates IDV, the idv-completed
event flips the IDV record, party-processor reacts to
the IDV changelog and activates the party). See
ADR-0008 and
parties.
Cold-start — bank-migrator-service applies FDB
record metadata and Pulsar topics/schemas;
bank-bootstrap-service seeds the singleton internal
organisation and the platform/micro policies. Both run as
one-shot k8s Jobs; services wait on the bootstrap Job
before starting. See
deployment.
The bank is documented:
- docs/prd/ — product requirements documents: a platform-wide umbrella plus one per capability (onboarding, parties, cash-account-products, cash-accounts, payments, interest, policies). The what and why — intended scope, users, and domain rules — companion to the TDDs' how.
- docs/tdd/ — technical design documents covering the substrate (transaction processing, transactions and balances, traceability, scenario testing, idempotency proposal), the API surface and auth (service-apis, api-keys), the policy engine, and every domain (organisations, parties, products, accounts, payments, interest).
- docs/adr/ — architecture decision records (mono fork, FoundationDB, message-bus abstraction, Avro, anomalies, kebab-case keys, system-as-data, changelog watchers, model-equality testing, code generation via prep-lib, one-component-per-library, pre-commit hooks, single unified API, OpenAPI 3.x compliance, comments and docstrings).
- docs/slides/ — a slidev walk-through of how systems-as-data assembles a running system.
- docs/recipes/ — task-oriented recipes (Problem / Solution / Rules / Discussion / References) for components, bases, projects, system-components, system-configurations, testcontainers, error-handling, testing, code-style, code-generation, common-helpers, deployment, git-workflow, writing-docs.
Start a REPL with just repl and connect your editor. The
development entry point follows the standard Polylith pattern —
a namespace under development/src/dev/ that requires the base
and Testcontainers:
;; development/src/dev/bank_monolith.clj — evaluate the comment block
(def sys
(main/start "classpath:bank-monolith/application-test.yml" :dev))
(main/stop sys)This boots the full system — FDB, Pulsar, HTTP server — inside Testcontainers. Then start the Svelte front-end:
just bank-app-startQueenswood is a domain fork of
mono, a Clojure component
library for production-ready distributed systems built on
Polylith. Bricks prefixed
bank-* are Queenswood-specific; everything else is shared
infrastructure inherited from upstream and pulled down via
git merge upstream/main.
See ADR-0001 for the
reasoning. The shared component library (lifecycle,
persistence, messaging, security, etc.) is documented in the
mono README.
For the workspace layout, see components/, bases/, and
projects/. Brick conventions are documented in
recipes/components.