System Design
How four production systems are structured — from request to database — and the trade-offs behind each architectural decision.
H-Phsar — B2B Marketplace API
A B2B online marketplace API connecting distributors and retailers — stores, product catalogs, carts, an order state machine, and real-time notifications. Spring Boot 3 on PostgreSQL.
Architecture decision
Every domain is split by role: distributors and retailers get separate controllers, services, and notification tables over one shared schema of 24+ tables. Orders move through an explicit status-driven state machine, so an order can never skip a step or be acted on by the wrong side of the marketplace.
Challenges
- Modeling one marketplace for two very different roles without duplicating business logic
- Guaranteeing an order can only move forward through its lifecycle, whoever is acting on it
- Verifying real users on a platform where trust between strangers is the product
Solutions
- Role-split API surface (distributor/* vs retailer/*) over one shared schema, with role-scoped queries
- A status-id state machine drives every order transition, so accept, dispatch, and deliver are the only legal moves
- Email OTP verification before an account can trade, with JWT sessions after
Lessons learned
- An explicit state machine turns business rules into something you can read, test, and audit
- Designing the API per role keeps permissions simple — the URL already says who may call it
- Documenting the schema (relationships, cascades) is as valuable as documenting the API
Hengo — AI Companion for Daily Growth
An AI-powered Korean learning and growth platform for software engineers and international professionals working in Korea.
Architecture decision
Most data flows directly from the browser to Supabase under Row Level Security. Only AI work reaches server routes, where the caller’s Supabase JWT is verified before any model request or persisted result.
Challenges
- Combining learning, interview, and productivity tools without creating a crowded interface
- Keeping AI requests secure while most application data is accessed directly from the browser
Solutions
- Organized features into focused workspaces with Today’s Mission as the primary daily entry point
- Kept data behind Supabase RLS and routed only AI calls through JWT-verified Next.js handlers
Lessons learned
- A large product becomes easier to use when navigation follows user intent instead of feature count
- RLS-backed direct data access and thin AI routes keep the architecture understandable
Money Flow — Personal Finance PWA
A personal finance PWA with budgets, savings goals, recurring transactions, AI chat over your finances, and push notifications. Next.js on Supabase with a Neon backup database.
Architecture decision
Row Level Security enforces per-user data access in the database itself, not just in application code. Background work runs as six secret-protected Vercel crons — recurring transactions, budget alerts, monthly email reports, savings updates, exchange-rate cleanup, and a daily full sync of every table to a Neon backup database.
Challenges
- Running real background jobs — recurrence, alerts, reports, backups — on a serverless platform with no always-on server
- Protecting years of personal finance data against a single-database failure
- Enforcing per-user data isolation more strongly than application-level checks
Solutions
- Six Vercel cron jobs, each a bearer-secret-protected API route, cover recurring transactions, budget alerts, monthly reports, savings updates, and cleanup
- A daily cron syncs every table to a second Postgres (Neon), so recovery never depends on one provider
- Supabase Row Level Security scopes every row to its owner at the database layer
Lessons learned
- RLS moves the security boundary into the database — an application bug can no longer leak another user's rows
- Serverless crons are enough for real background work when every job is safe to re-run
- A backup you haven't automated is a backup you don't have
We Commerce — Multi-Vendor Marketplace
A full-stack multi-vendor e-commerce marketplace: Spring Boot 3.4 / Java 21 REST API with database-tracked JWT auth, and a Next.js 16 storefront with cart, checkout, and simulated Cambodian payment flows (ABA Pay, KHQR).
Architecture decision
Every endpoint returns one uniform envelope — { payload, message, code, error } — so the frontend unwraps all responses identically. Auth tokens are persisted server-side, which makes logout an actual revocation instead of just deleting a client token. And the storefront falls back to mock data whenever the API is empty or unreachable, so the UI is demoable out of the box.
Challenges
- Making logout actually mean something when auth is stateless JWT
- Keeping save/bookmark interactions feeling instant on slow connections
- Demoing a storefront whose backend may be empty or asleep on free-tier hosting
Solutions
- Tokens are tracked in the database and revoked on logout — the security filter rejects revoked tokens even before they expire
- TanStack Query optimistic updates flip the bookmark state immediately and roll back on failure
- A mock-data fallback layer serves the storefront when the API is empty or unreachable
Lessons learned
- Pure stateless JWT is a trade-off — one database check per request is often worth having real revocation
- A single response envelope across every endpoint removes a whole class of frontend special-casing
- Optimistic updates are the cheapest big UX win in a CRUD app
AuthHub — Reusable Authentication Service
A multi-module Spring Boot 3.5 monorepo providing authentication as a reusable service: JWT with refresh and real revocation, Google ID-token login, MFA (TOTP + backup codes), RBAC, audit logging, and rate limiting — plus a sample business API that consumes it.
Architecture decision
Dependencies flow one way — common-api → security-api → todoapi — so the auth service can never depend on a business API. The schema belongs to Flyway: Hibernate only validates it, which turns silent schema drift into a startup failure. Auth state that must be revocable (refresh tokens, blacklisted JWTs, reset/verification/unlock tokens) lives in dedicated tables rather than in stateless token claims.
Challenges
- Making logout and token theft recovery real when JWTs are stateless by design
- Adding MFA as a second login step without breaking existing password-only clients
- Evolving the schema safely across modules without Hibernate silently altering tables
Solutions
- Refresh tokens and a revoked-token blacklist are tracked in PostgreSQL — the security filter rejects revoked JWTs before expiry
- MFA is an explicit verify step after password login, with hashed single-use backup codes as the recovery path
- Flyway versioned migrations own the schema and Hibernate runs validate-only, so drift fails the build instead of hiding
Lessons learned
- Authentication is a product, not a feature — extracting it once beats re-implementing it in every project
- Real security work is mostly edge cases: lockout, resend limits, token expiry, and recovery paths
- ddl-auto=validate plus Flyway makes schema drift impossible to miss — the app refuses to start on a mismatch
Dev Lab — Personal Practice & Tooling Monorepo
A personal lab of independent practice projects and learning notes — Spring Boot topics by area (web, data, security, messaging, testing, cloud, AI), core Java exercises, and small full apps, each in its own folder.
Architecture decision
There's no single request path here — Dev Lab's 'architecture' is its repo layout, not a runtime flow. Each folder is a standalone project with its own build tool and wrapper, so any one of them opens and runs in IntelliJ without touching the others.
Challenges
- Keeping years of unrelated practice projects organized and buildable without them interfering with each other
- Consolidating four previously separate repositories into one without losing project-level history
Solutions
- One folder per project, each with its own build tool and wrapper, documented in a root README table
- Merged the four source repositories in June 2026, keeping the older commit history archived in the original repos
Lessons learned
- A practice repo needs the same hygiene as a product repo — without one folder per project and a README table, old work becomes unfindable
- Keeping each sub-project on its own build tool and wrapper is what let four repos merge without any of them breaking
- Revisiting old auth code after building AuthHub for real showed how much a second pass changes what 'production-ready' means