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 — Focused Korean Learning Platform
A focused AI-assisted Korean learning platform for software engineers and international professionals living and working in Korea.
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.
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).
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
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.
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