exit lab
~/lab/systems

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.

case study
Spring Boot 3Java 17MyBatisPostgreSQLSpring SecurityWebSocket
Spring Security + JWT
Spring Boot 3 REST API
MyBatis mappers
PostgreSQL + Flyway
WebSocket notifications

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.

case study
Next.js 16React 19TypeScriptSupabaseVercel AI SDKOpenAI
Next.js 16 SPA
Supabase Auth + RLS
Supabase Postgres
Next.js AI routes
OpenAI via Vercel AI SDK

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.

case study
Next.jsTypeScriptSupabaseNeonGoogle GeminiWeb Push
Next.js 16 (App Router)
Supabase Auth + RLS
Supabase Postgres
Vercel Cron Jobs
Neon backup · Gemini AI

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).

case study
Spring Boot 3Java 21PostgreSQLNext.jsTanStack QueryZustand
Next.js 16 storefront
Axios + JWT interceptor
Spring Boot 3.4 REST API
Spring Security (DB-tracked JWT)
PostgreSQL 16 + JPA

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.

case study
Spring Boot 3Java 17PostgreSQLSpring SecurityJPAFlyway
common-api shared library
security-api — JWT · MFA · audit (8080)
todoapi sample consumer (8082)
PostgreSQL + Flyway migrations
GitHub Actions CI (postgres:16)

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.

case study
Spring BootJava 21MyBatisPostgreSQLMavenGradle
java-core-mastery-lab — Java 21 core exercises
spring-boot-lab — Spring Boot topics by area
heang-dev-lab — Spring Boot + MyBatis store admin
heang-api-center — Spring Boot API practice
spring_jwt_authentication — JWT auth practice

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