kubernetes-banking-platform: How to Keep a Banking App Correct on Kubernetes
A reference architecture that treats Postgres replication, pod placement, and transaction locking as first-class parts of the same system.
- This repository treats banking correctness as a distributed systems problem, not a deployment problem.
- The API separates reads from writes and uses explicit row locking to stop double-spend bugs before they start.
- The database setup is the real architecture, with stateful storage, node separation, and manual replication doing the heavy lifting.
- The setup script and security manifests turn the cluster into a failure lab instead of a polite demo.
The hard part of a banking app is not the UI
A dashboard is easy. The hard part is making sure a balance is still true after restarts, retries, replica lag, and horizontal scaling. This repo is interesting because it treats that problem as the point of the exercise, not as an edge case to wave away.
That is why the project feels more like a correctness lab than a product demo. It asks a sharper question than most Kubernetes examples: can the system still refuse bad money movement when the platform itself is under stress?
The API knows the difference between reads and writes
The backend does not spray every request at the same database connection. It splits traffic into two pools: writes go to the primary, and reads go to the replica. That keeps the system honest about consistency, instead of pretending every query has the same urgency.
app.post('/api/transactions', async (req, res) => {
const client = await primaryPool.connect();
try {
await client.query('BEGIN');
const account = await client.query(
'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE',
[req.body.accountId]
);
if (account.rows[0].balance < req.body.amount) {
throw new Error('Insufficient funds');
}
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[req.body.amount, req.body.accountId]
);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
});
That `FOR UPDATE` matters more than the framework around it. It turns a concurrent balance update into a serialized decision, which is exactly what you want when the bug you are trying to prevent is double spend.
Kubernetes allowed us to clearly define responsibilities between the product and ops teams. It provided the primitives necessary - such as Services, Deployments and ConfigMaps - to roll our software out in a fast, predictable manner.
The database is the real system, not a sidecar
The most revealing part of the repo is not the app code. It is the PostgreSQL setup, because that is where the guarantees come from. Stateful storage, stable pod identity, and anti-affinity keep the primary and replica from collapsing into the same failure domain.
| Dimension | Manual educational stack | Operator-managed stack |
|---|---|---|
| Postgres management | Custom replication bootstrap and explicit StatefulSet behavior | Automated lifecycle control with more hidden machinery |
| Startup coordination | Init containers, wait loops, and readiness checks are visible | Startup ordering is mostly abstracted away |
| Operational complexity | More YAML and shell, but every seam is inspectable | Less bespoke code, more platform magic to trust |
| Educational value | High, because you see where correctness comes from | Lower, because the mechanism is often a black box |
That tradeoff is the point. The repo is not trying to out-automate an operator. It is trying to make the failure model legible.
setup.sh is the hidden architecture
The setup script is not a convenience wrapper. It is the part that turns a local cluster into a believable system. It validates tools, starts Minikube, applies labels and taints, tunes the host, and generates runtime config so the rest of the manifests can assume a more realistic environment.
#!/usr/bin/env bash
set -euo pipefail
check_tools() {
command -v kubectl >/dev/null
command -v docker >/dev/null
command -v minikube >/dev/null
}
start_cluster() {
minikube start --cni=calico --nodes=2
kubectl label node minikube type=high-memory
kubectl taint nodes minikube dedicated=database:NoSchedule
}
wait_for_db() {
kubectl wait --for=condition=ready pod -l app=postgres --timeout=180s
}
check_tools
start_cluster
wait_for_db
That kind of automation matters because it forces the system to meet operational reality halfway. You are not just deploying manifests. You are rehearsing a cluster that can fail in the same ways a real one does.
Security is baked into the manifests, not bolted on
The security posture is practical rather than performative. Non-root containers, dropped capabilities, a read-only filesystem, ingress rate limiting, and network policy isolation all narrow the blast radius if something goes wrong.
| Control | What it prevents | Why it matters |
|---|---|---|
| Non-root user | Privilege escalation inside the container | A compromised process has less room to move |
| Read-only filesystem | Tampering with the application image at runtime | Keeps runtime behavior closer to what was shipped |
| NetworkPolicy | Unauthorized pod-to-pod traffic | Reduces lateral movement across services |
| Ingress rate limiting | Request floods at the edge | Adds a cheap first line of defense |
This is a good example of security as deployment design. The manifests do not just say the app is safe. They shape what the app can do.
Why this is more instructive than a Postgres operator
A production operator may be better automation. This repo is better teaching. It shows the moving parts plainly, so you can see why startup ordering, storage identity, and transaction discipline are all part of the same problem.
The core philosophy of this project is Security by Default and Zero Trust Architecture. It eliminates hardcoded credentials, enforces least-privilege IAM access, and ensures strict container runtime security.
That is the useful lesson here. The repo is not a polished banking platform you would copy into production as-is. It is a blueprint for understanding how correctness survives when every layer can fail.
What this repository is really for
The best reading of this project is simple. It is a lab for learning how stateful systems behave on Kubernetes, with money as the stress test. If you understand why this stack needs explicit locks, stable database identity, startup coordination, and security boundaries, you understand the heart of the architecture.
That is the real value. Not a prettier demo. A clearer mental model for keeping a banking app trustworthy when the platform stops being polite.





