❯ Fintech · Platform · 2024
Vanguard-Bank
A full-stack digital banking platform for retail and SMB users: a Flutter app over six Go microservices on an event-driven core.
The problem
Building a bank is not one product but several: accounts, transfers, cards, notifications, and fraud signals all change and scale at different rates. Tied together in one deployable, a card rollout stalls a transfer fix. The platform needed real boundaries from day one, not after a rewrite.
The approach
Six Go microservices, each owning one bounded context (auth, account, transfer, card, notification, analytics), behind a Go + Chi gateway. Clients speak REST; services speak gRPC/Protobuf to each other; NATS JetStream carries events with seven-day retention, a dead-letter queue, and five max deliveries. Money never travels as floats: every balance is integer minor-currency units.
Architecture
- Flutter Mobile App
- Go + Chi Gateway
- Bounded-Context Services
- NATS JetStream
- PostgreSQL / Redis
- Prometheus + OTel
FLOWS
- Flutter Mobile AppRESTGo + Chi Gateway
- Go + Chi GatewaygRPCBounded-Context Services
- Bounded-Context ServicesSQL / cachePostgreSQL / Redis
- Bounded-Context ServiceseventsNATS JetStream
- NATS JetStreamretry + DLQBounded-Context Services
- Bounded-Context ServicesmetricsPrometheus + OTel
The gateway terminates client auth and forwards requests to services over gRPC. Transfers are the hard path: Redis-backed idempotency keys with a 24-hour TTL stop double-submits, business accounts require dual approval, and balance operations stay atomic. The card service generates Luhn-valid PANs, encrypts CVVs until a biometric-verified reveal, and clears the clipboard after sixty seconds. A fraud-signal service consumes the event stream, and notifications fan out per category over FCM and APNs. Auth is the banking-grade layer: RS256 JWTs with 15-minute access and rotated 7-day refresh, TOTP MFA, bcrypt cost 12, device binding, and rate-limited endpoints. Observability is slog JSON logging, Prometheus metrics, and OpenTelemetry tracing through the whole graph.
Key decisions
Bounded contexts as the unit of scale
Card work never blocks a transfer deploy, and each service fails over independently.
Event bus over direct calls
JetStream retention plus the DLQ makes "we lost the event" a solved problem instead of a postmortem.
Integer minor-currency everywhere
No float rounding, no "just this one time" money bugs.
Clean Architecture on the client
Presentation, domain, and data layers in Flutter with the full testing pyramid so a UI refactor cannot break money logic.
What I'd do differently
The analytics service arrived late, after the event schema had already stabilized. I would wire it into the first transfer flow instead of bolting it on, and version the NATS subjects from the start.