Skip to content

kjothen/queenswood

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

793 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Queenswood

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

Capabilities

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: openingopenedclosingclosed
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 pendingactive (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.

What's interesting

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.

Architecture

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
Loading

HTTP servicesbank-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 openingopened and closingclosed; 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-startbank-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.

Documentation

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.

Running

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-start

Built on mono

Queenswood 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.

About

Queenswood banking

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors