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 — Focused Korean Learning Platform

A focused AI-assisted Korean learning platform for software engineers and international professionals living and working in Korea.

case study
Next.js 16React 19TypeScriptSupabaseVercel AI SDKOpenAITailwind CSS
Next.js 16 + React 19 SPA
Supabase Auth + RLS
TanStack Query + domain API clients
Next.js AI route handlers
OpenAI via Vercel AI SDK

Architecture decision

Feature data goes directly from browser-side domain clients to Supabase under Row Level Security. AI work is the only server-side application path: the route verifies the caller with a per-request Supabase client before generating or persisting feedback. There is no separate backend service in the deployed web application.

Challenges

  • Reducing a broad platform to a focused Korean-learning product without deleting working features or user data
  • Keeping AI requests secure while most application data is accessed directly from the browser

Solutions

  • Made Today, Vocabulary, Practice, Coach, and Study the only primary destinations while preserving hidden routes and the full route registry
  • Kept data behind Supabase RLS and routed only AI calls through JWT-verified Next.js handlers with per-user limits

Lessons learned

  • A product becomes easier to return to when its first screen answers one clear daily question
  • Hide-and-preserve is a safer product migration strategy when existing routes and user data still have value

Luyra — Personal Finance Workspace

A personal finance workspace for money tracking, budgets, savings, analytics, and recurring financial reviews — built as the active product surface of Luyra.

case study
Next.js 16React 19TypeScriptSupabase AuthNeon PostgresVercel CronAnthropic
Next.js 16 + React 19
Supabase Auth
Next.js API route handlers
Services + repositories
Neon Postgres · Vercel Cron/Workflow

Architecture decision

Authentication and application data have an explicit boundary: Supabase owns the session and JWT identity, while authenticated route handlers derive the user id server-side before services and repositories query Neon. That boundary also leaves a contained path for the planned Spring Boot API migration.

Challenges

  • Migrating a personal finance product toward a clearer auth/data boundary without coupling the UI to database code
  • Making financial reviews and scheduled notifications useful while keeping every job authenticated and rerunnable
  • Protecting sensitive finance records when application data moves from the legacy Money Flow store to Neon

Solutions

  • A thin lib/api → route handler → service → repository pipeline validates input, checks auth, derives user_id server-side, and keeps SQL isolated
  • Vercel Cron and Workflow jobs handle budget alerts, spending spikes, reminders, weekly summaries, and monthly reports as independently authorized routes
  • Neon is the native application database while Supabase remains the identity provider, giving Luyra a clean path toward a future Spring Boot backend

Lessons learned

  • A deliberate boundary between identity, HTTP, business logic, and SQL makes a backend migration much smaller
  • Financial products become more useful when analytics and review workflows turn raw transactions into decisions
  • Scheduled notifications are product features when their timing, delivery, and failure states are designed as carefully as the dashboard

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

VSTV Agent — Real Estate Platform & Operations Console

A public real-estate platform and private staff workspace for VSTV Agent: property discovery, agent contact, unit inventory, CSV import, and scheduled Telegram publishing for Phnom Penh rentals.

case study
Next.js 14React 18TypeScriptTailwind CSSDrizzle ORMNeon PostgreSQLSpring BootTelegram
Next.js App Router public site + admin console
Typed browser API clients
Next.js route handlers + application services
Drizzle ORM + Neon PostgreSQL
Spring Boot API during incremental migration
Telegram scheduled delivery

Architecture decision

The UI is deliberately insulated from backend ownership: screens call lib/api clients, which call /api/v1 paths. Some paths still reach Spring Boot while migrated domains run through Next.js route handlers, services, repositories, and Drizzle. Private owner fields stay server-side and never enter public property or Telegram payloads.

Challenges

  • Moving backend ownership incrementally without breaking a live public site or coupling browser components to database details
  • Keeping private owner contacts and internal notes out of public listings and Telegram notifications
  • Making imports and scheduled delivery safe to retry without duplicate inventory or duplicate broadcasts

Solutions

  • A stable /api/v1 boundary with shared clients lets Spring Boot and Next.js route handlers migrate domain by domain
  • Server-side response mapping and a public-safe Telegram formatter explicitly omit sensitive fields before delivery
  • Preview/confirm imports, durable daily claims, and idempotent delivery logs make long-running operational workflows recoverable

Lessons learned

  • Incremental backend migration works best when the client contract remains stable and ownership is documented per domain
  • Operational safety is part of product design: preview, permissions, redaction, retries, and audit records matter as much as CRUD screens
  • A public fallback should be honest about missing inventory rather than displaying data that staff have not published or verified