Personal engineering portfolio

A commerce case study focused on interface behavior, product search, checkout flow, and defensible backend boundaries.

Stack: Java 21, Spring Boot, PostgreSQL + pgvector, Kafka, Next.js, React

I built this project to show how interface work, data flow, and backend boundaries fit together in a storefront. You can browse products, search in plain English, build a cart, and place an order through a checkout backed by a Stripe test-mode payment and one verified event boundary. Each boundary is a decision I can explain and defend.

System shape

Gateway + BFFs + domain services, with one defended async boundary

System shape — gateway, BFFs, domain services, one Postgres, one async boundaryBrowser to api-gateway to three BFFs (storefront, cart, checkout), each fronting a domain service (product, cart, order). All domains persist to one PostgreSQL instance with per-service databases; AI lives only in product-domain. The checkout path reaches payment-domain and Stripe; the only async boundary is the verified Stripe webhook, published as one Kafka payments.results fact that two consumer groups read.browserapi-gatewayanonymous · OAuth / CSRF · X-User-IdBFFs shape UI flowsstorefront-bffbrowse · searchcart-bffcart opscheckout-bffSSE projectionproduct-domainsemantic search · OpenAIcart-domainquantity ledgerorder-domaindurable orders · sole writerdomains own statePostgreSQL — one instance, per-service databasesproduct_db · cart_db · order_db — pgvector lives in product_dbpayment-domainStripe adaptercreate PaymentIntentStripepayment finalitywebhookverify + publishKafka payments.resultsone verified factthe only async boundary2 consumers: durable status + SSEBFFs shape UI flows · domains own state · one Postgres, per-service databases · Kafka only after the verified Stripe webhookthe one async boundary (Kafka)external — Stripe / OpenAIour services + Postgres

Design choices

A few choices that shaped the project

Next.js · React SPA

Two frontends, one API contract

The Next.js UI is the portfolio storefront with server-rendered architecture context; the React SPA is a separate browser-rendered storefront. Both use the same gateway routes for browse, search, cart, checkout, and SSE order progress — no SPA-only backend adapter.

Open the React SPA ↗

LLM parsing · pgvector retrieval · grounded explanation

Search with one honest path

Search starts with plain English. An LLM extracts structured filters, pgvector retrieves catalog items, and Postgres reloads product-domain truth. A separate grounded call explains results it cannot add, remove, or re-rank. There is no hidden keyword fallback.

Stripe webhook · one Kafka event · SSE

Checkout with a defended async boundary

Place Order is a synchronous REST chain to a Stripe test-mode checkout boundary. Duplicate submits, lost payment responses, and webhook redelivery are handled with idempotency and locking, without turning checkout into a saga. Kafka appears only after the verified webhook.

Preview the checkout pipeline

Architecture preview

Representative checkout pipeline

Public preview of the same shape used after checkout: sync Place Order, Stripe test-mode PaymentIntent, verified webhook, one Kafka fact, durable order status, and SSE push.

Toggle diagram
Checkout slice — synchronous order creation and the one verified Kafka webhook factPlace order is a synchronous REST chain: browser to checkout-bff to order-domain, which writes the durable order to Postgres and calls payment-domain to create a Stripe PaymentIntent. The sync request returns before payment finality. Later, Stripe finalizes payment out of band and sends one verified webhook; payment-domain verifies the signature and publishes one Kafka fact on payments.results, which two consumer groups read — order-domain updates durable status and checkout-bff pushes SSE to the browser.place order · synchronous · user is waitingbrowserplace ordercheckout-bffsse projectionorder-domaindurable orderspayment-domainstripe adapterStripepayment finalityPaymentIntentpostgresowns durable order stateINSERT → UPDATEdurable, sole writerthe async boundary — Stripe finalizes payment out of band, then ONE verified webhookpayment result · asynchronous · one webhook factStripe webhookpayment finalizedpayment-domainverify signaturepayments.resultsone verified factverifiedorder-domaindurable status updatecheckout-bffSSE pushbrowserawaiting webhook2 consumer groups→ same Postgresrepresentative in-flight state — on a real order, processing timings appear here once the webhook arrivesone verified Kafka fact · async boundaryexternal — Stripe owns payment finalityour services + PostgresKafka carries one verified fact, not commands · one topic, two consumer groups · ADR-008checkout-bff owns the SSE projection, not order truth · no saga · no outbox · no broad topology

Representative data only. This is not a scenario simulator: no saga, no outbox/inbox, no inventory reservation, and no broader Kafka topology are hidden behind the diagram.

Try the search

Search in plain English

Try something like "running shoes under $120". The search is designed to accept everyday language while still staying grounded in the catalog.

David Lee

Spring Boot · Next.js · Spring AI · PostgreSQL · Kafka

© 2026