Portfolio project

Architecture
is the
resume.

Ledger is a personal finance app built to production-grade standards. Not to compete with Mint, but to demonstrate how I think about systems.

The feature set is a vehicle. The architectural decisions are the point.

Why personal finance?

Mint is dead. The space is crowded. That's not the point. Personal finance naturally justifies real architecture. Bank connectivity requires compliance thinking, budgeting requires domain modelling, and multi-account aggregation requires event-driven design. The domain earns every pattern in the codebase. A todo app wouldn't.

If real users show up, the foundation is ready. Plaid is real. The infrastructure is production-grade. Compliance and cost would be the conversation at that point, not the architecture.

Case studies

The pivots, the migrations, and the honest accounting of what was tried before the current approach.

The decisions worth talking about

Every pattern here has a rationale. These are the answers to the “why” questions.

Frontend architecture

Feature-Sliced Design (lite)

The layered model and one-way dependency rules from FSD, without the full specification overhead.

Transport layer

Next.js server actions via next-safe-action

Composable middleware, schema validation, and a single catch boundary via next-safe-action.

System design

Modular monolith over microservices

Service boundaries drawn too early are a bet on requirements you don't have yet.

Domain layer

Domain-Driven Design (lite)

A finance domain has real invariants worth modelling explicitly. A todo app wouldn't justify this.

Application layer

CQRS with a typed Command Bus

Commands and queries are separate concerns. The bus makes dispatch type-safe without boilerplate.

System design

CQRS read model

A separate read model is justified. A separate database is not, at least not yet.

Infrastructure

In-process event bus

Domain events decouple what happened from what happens next. The interface hides the delivery mechanism.

Infrastructure

Durable event bus with async dispatch

Every event hits Postgres before any handler runs. QStash handles async delivery.

Infrastructure

Event handler ordering

Registration order is implicit coupling. It works because processing is sequential. An explicit event chain would make the dependency visible.

Security

JWT authentication

Short-lived JWTs carry only userId. A type claim separates access tokens from MFA challenge tokens without multiple signing keys.

Security

TOTP-based multi-factor authentication

Two-step login with challenge tokens, aggregate-owned events, and service-layer signing. No auth logic in handlers.

Infrastructure

Feature flags

Tier-gated, cache-aside, zero vendor. A database table and a Redis cache replace a SaaS subscription.

Infrastructure

OpenTelemetry + Grafana Cloud

Open standards for instrumentation. The backend is a config swap, not a rewrite.

Infrastructure

Two-tier health checks

A shallow probe keeps the orchestrator happy. A scheduled deep check catches silent dependency failures before users do.

Explore the project