C4 and component views for RetailBankingCore. Portfolio service, not a production bank core.
C4Context
title Retail Banking Core - System Context
Person(user, "Banking user", "Registers, opens accounts, transfers money")
System(core, "Retail Banking Core", "JWT auth, accounts, double-entry transfers, domain events")
SystemDb(pg, "PostgreSQL", "Users, accounts, transfers, ledger, outbox")
SystemDb(redis, "Redis", "Distributed transfer rate limit")
System_Ext(kafka, "Kafka", "Domain event bus")
Rel(user, core, "HTTPS / JSON")
Rel(core, pg, "JDBC via HikariCP")
Rel(core, redis, "Token bucket Lua")
Rel(core, kafka, "Outbox relay publish")
C4Container
title Retail Banking Core - Containers
Person(user, "Banking user")
Container(api, "Spring Boot API", "Java 17 / Spring Boot 3.3", "Auth, accounts, transfers, outbox relay, Actuator")
ContainerDb(pg, "PostgreSQL 16", "Flyway V1-V4", "Durable store + transactional outbox")
ContainerDb(redis, "Redis 7", "In-memory", "Per-user transfer rate limit")
Container_Ext(kafka, "Apache Kafka", "Message bus", "banking.domain-events topic")
Container_Ext(ci, "GitHub Actions", "CI", "mvn verify + Testcontainers")
Rel(user, api, "HTTPS")
Rel(api, pg, "SQL")
Rel(api, redis, "EVAL token-bucket")
Rel(api, kafka, "producer (outbox relay)")
Rel(ci, api, "build / test")
flowchart TB
subgraph http [HTTP]
AuthC[AuthController]
AccC[AccountController]
TrfC[TransferController]
Health[/actuator/health]
end
subgraph security [Security]
JwtF[JwtAuthenticationFilter]
RateF[RateLimitFilter Redis]
end
subgraph app [Services]
AuthS[AuthService]
AccS[AccountService]
TrfS[TransferService]
JwtS[JwtService]
OutboxPub[OutboxEventPublisher]
Relay[OutboxRelay]
end
subgraph data [Persistence]
Users[(app_users)]
Accounts[(accounts)]
Transfers[(transfers)]
Ledger[(ledger_entries)]
Outbox[(outbox_events)]
end
subgraph infra [Infrastructure]
Redis[(Redis)]
Kafka[(Kafka topic)]
end
AuthC --> AuthS --> Users
AuthS --> JwtS
AccC --> AccS --> Accounts
AccS --> Ledger
AccS --> OutboxPub
TrfC --> RateF --> TrfS
RateF --> Redis
TrfS --> Accounts
TrfS --> Transfers
TrfS --> Ledger
TrfS --> OutboxPub
OutboxPub --> Outbox
Relay --> Outbox
Relay --> Kafka
JwtF --> JwtS
- Business TX (transfer / account change) writes ledger + audit and an
outbox_eventsrow. - After commit,
OutboxRelayclaims with short TX (claimed_at,FOR UPDATE SKIP LOCKED). - Kafka publish runs outside that TX (no long-held row locks under network I/O).
- Second short TX marks
published_at(at-least-once; de-dupe oneventId). - Stale claims are reclaimable; poison rows after max attempts are logged.
| Event type | Aggregate | When |
|---|---|---|
ACCOUNT_OPENED |
ACCOUNT | Customer account open |
ACCOUNT_FROZEN / UNFROZEN / CLOSED |
ACCOUNT | Status change |
TRANSFER_POSTED |
TRANSFER | Successful transfer (not idempotent replay) |
TRANSFER_REVERSED |
TRANSFER | Compensating reversal |
- Redis Lua token bucket on
POST /api/transfersandPOST /api/transfers/{id}/reverse. - Key: authenticated username (or remote IP if unauthenticated).
- Default fail mode CLOSED: Redis down returns HTTP 503 (no open flood window).
OPENING: HOUSE funding DEBIT + customer CREDIT, sharedjournal_idTRANSFER/REVERSAL: paired lines withtransfer_id; reversal never mutates old rows- Ledger and audit tables are append-only (Postgres triggers)
accounts.balance= Σ CREDIT − Σ DEBIT; global Σ DEBIT = Σ CREDIT
- Local: Compose (Postgres + Redis + Kafka + optional app container)
- CI: JVM + Testcontainers (Postgres, Redis, Kafka)
- Schema only via Flyway (
V1–V5), Hibernateddl-auto=validate - OpenAPI UI:
/swagger-ui.html