Systems rarely break on the technology. They break on the trade-offs nobody made explicit. So I design the decisions before the technology.
For over three years I've worked in domains where the output goes straight into a client's external reporting — ESG emissions, carbon reduction, life-cycle assessment. My part was the stretch that takes sources you can't trust — paper forms, ERP ledgers, waste-transfer manifests — and turns them into official numbers. Double-counting, silent failures, grain distortion: I met all three in production and fixed them.
Making the probabilistic (LLMs) deterministic, and the precarious (migrations) lossless.
So far I've done this where accuracy is hard. Next I want the same problem where scale and concurrency are in play too — payments and settlement, where the number is the money — and to learn consistency under real traffic.
Focus
Consistency under scale
Settlement & idempotency
Event-driven architecture
Domain modeling
Also work across
Data integrity & grain
DB & runtime migration
LLM pipeline reliability
Enterprise integration (SAP, LDAP)
API & DB design
Cache & queue design
Engineering Principles
01
Appropriate design is the goal — not the technology.
On the ESG platform I proposed Node.js into a Java/Spring shop — and carried the decision not on preference but on documented evidence: Worker Threads and an I/O-bound workload profile. The domain itself was modeled on OpenLCA to reuse an existing standard.
02
AI is a tool. The operational system is the product.
The OCR pipeline uses AI only where deterministic rules can't reach, and keeps it verifiable — grounded in two OCR engines' confidence and diffs, with low-confidence cells surfaced for a human final check. Never a black box.
03
"Done" means stable in operation — not "shipped."
I shipped handling for a serverless connection failure — then confirmed in production that it wasn't actually firing. A shared error translation was erasing the original message the matcher depended on. It wasn't finished until I found the cause again and fixed it.
04
I don't claim an improvement I didn't measure.
Improving load performance, no way to measure it existed — so I built the bench harness first, measured per tier, and wrote down the measurement conditions before drawing any conclusion. Going the other way: when I found a figure in an older document that had been written without instrumentation, I went back for the evidence and withdrew it.
The extracted value becomes the official figure — and if it's quietly wrong, nobody finds out.
whole table → diff cells
gate 0.9 · observed max 0.74
Extracts product-usage data (e.g. cement volume) from factory forms. Irregular layouts and merged cells were beyond rule-based parsing, so I designed a pipeline that hands two OCR engines' results, confidence scores, and diffs to an AI layer for correction and adjudication — making AI a judgment component in the system, not just automation.
NestJS
GraphQL
AWS
Redis
Supabase
OpenAI
Clova OCR
AWS Textract
Claude Code
Result
Redesigned the AI payload from whole tables down to diff cells. The old shape handed both OCR results over in full and asked the model to rewrite the finished table; the new one sends two header rows and the disagreeing cells, and the backend does the merge. A per-column sample cap keeps the prompt from growing linearly with the table. On top of that: confidence-based skipping of the second OCR, model selection by document complexity, and caching of re-analysis.
Takeaway
Where deterministic rules couldn't reach, an AI judgment call filled the gap — but I removed the logic that treated the model's self-reported confidence as a trust value, because nothing backs it. Confidence comes only from the vendor. And when the call is uncertain the pipeline doesn't skip; it invokes both engines. Ambiguity falls toward the expensive side, by design.
for a large petrochemical manufacturer · small backend team
A million rows, 80 at a time, is 12,500 round trips.
1M rows in 23.5s (dev env)
12,500 round trips → 1
Rising demand from overseas customers for environmental data had outgrown Excel and manual work. I designed the ingestion for 10+ external system interfaces — SAP PI (SOAP), in-house utility and EHS systems, the national waste-transfer system, city gas — as a staging / refined / fact three-layer model, and coordinated data specs and server setup directly with the client's infra team.
NestJS 11
MSSQL
SAP PI (SOAP)
LDAP/AD
Windows Server
Nginx
GitHub Actions
Prometheus
Grafana
Loki
SonarQube
Claude Code
Result
Switched SAP CO (cost ledger) loading from 80-row chunked INSERTs to MSSQL Bulk Copy: 1M rows in 23.5s. No way to measure it existed, so I built the bench harness alongside (dev environment, synthetic data). Upstream systems retroactively correct and resend past data; I generalized that into a temporal + natural-key conditional MERGE and applied it across all 10 sources that carry a natural key.
Takeaway
The bottleneck wasn't ORM overhead — it was round trips. A million rows at 80 per call is 12,500 of them. But Bulk Copy is all-or-nothing, which makes it hard to tell which row failed. I could accept that cost only because the target was a pure insert and re-running was safe, so I didn't apply it where an upsert was needed. The correction history was a separate call: the four sources whose natural key isn't unique were left out of the conditional MERGE with the reason recorded, and for one of them I gave up on row-level correction and kept every period snapshot by point in time instead.
pgloader moves rows. It doesn't move a changed data model.
MySQL → PostgreSQL
AWS → Vercel/Supabase
shared pooler → dedicated
A live B2B service had outgrown its stack, so I led a migration that changed the data model — not just the host: MySQL→PostgreSQL, AWS→Vercel/Supabase.
MySQL
PostgreSQL
Supabase
Vercel
pgloader
Result
Worked through every query-dialect difference — identifier quoting, date-format functions, JOIN semantics — and hand-wrote the structural transforms pgloader couldn't do. The cutover ran inside a night-maintenance window; afterwards I fixed value errors found in emission-factor calculation and in table references. The connection failures that followed traced to the managed DB's shared connection pooler, and I moved to a dedicated pooler.
Takeaway
The risk in a migration isn't the copy — it's the parts that don't automate. pgloader moves rows; it doesn't move a changed data model. There is something I failed to leave behind here, though: no full-reconciliation report or checksums in writing, and while there's a record of defects found and fixed, how they were found never became a documented procedure. Next time I'd build the expected-value set for the core aggregate functions before the cutover.
Production was throwing intermittent connection timeouts. While a serverless function is frozen its JS timers stop, so dead connections stay in the pool; after thaw one gets checked out and drops. That was my read. I added a retry interceptor and graceful degradation, and shipped.
NestJS
PostgreSQL
Supabase
Vercel
Pino
Result
After shipping, I confirmed the interceptor wasn't actually firing. The project's shared error-translation layer was rewriting DB errors into a generic code and erasing the original message, so the pattern match never matched. I made stale-connection errors propagate untranslated, and gave a still-failing retry its own code so the frontend can tell the user something useful.
Takeaway
Had I stopped at root-cause analysis and a code change, this would still be broken. Two things pointed at the app rather than the platform: the stack trace sat on the existing-pool-connection path, not on new connection creation, and external clients connected fine at the same time. Conversely, a window in the same period where external connectivity was cut entirely is one I couldn't attribute — I stated what I had verified and closed there.
Led the backend for an ESG diagnostics and consulting platform aimed at a regional bank's SME customers — diagnostic scoring, a greenhouse-gas calculator, and reward-coupon issuance.
NestJS
GraphQL
PostgreSQL
Vercel
Cloud SMS/email API
Result
Found that the coupon transaction ID was timestamp-based and could collide under concurrent issuance, and moved it onto the reward record's UUID to get idempotency. Send failures are now branched by result code, with an alert to the dev team. The greenhouse-gas calculation got divide-by-zero guards, significant-figure rounding, and annual conversion.
Takeaway
A coupon sent twice is money. That a timestamp can order things but can't guarantee uniqueness is something I learned after I'd already built it that way — there was no reason to mint a new identifier when a record with guaranteed uniqueness already had one. The calculator is the same shape: the user sees a single number, and if it's quietly wrong behind that number, there's no way to notice.
A multi-agent planning pipeline that turns a stochastic, slow 8B local model into something reliable and cheap enough to actually use — a founder's rough idea into a PRD and architecture doc on a 16GB MacBook, at zero API cost. An 8B model is two problems at once: unreliable, and expensive to run. So I clamp it — a separate critic, a save-validation gate, and state isolation wrap each probabilistic step in a deterministic shell — and I squeeze it, instrumenting every model call to measure and cut what buys nothing rather than guess.
LangGraph
Ollama (qwen3:8b)
deepseek-r1:8b
FastAPI
SqliteSaver
LangSmith
Go (stdlib)
Result
Phase 0 (idea → PRD → architecture) runs end-to-end, fully local. Scope is deliberately capped at Phase 0 as a finished product, given an 8B model's code-generation ceiling.
Takeaway
Squeeze is measured, not guessed. Stripping `<think>` from the reply doesn't buy the time back — on-device, every token generated is already wall-clock, and every context window is RAM, so I instrument each model call instead of guessing. Clamp's counterpart: state isolation runs in two directions. Outbound (a subagent's internal turns polluting the parent thread) is solved with separate state and a RemoveMessage finalize. Inbound (the subagent's LLM still seeing parent messages) is a deliberate trade-off, compensated with a structured briefing packet — not hidden. Determinism comes from the graph, not the prompt.
Phase 1–6 (build / QA / deploy) exist as persona designs — roadmap, not yet wired. The gateway is 610 lines and has never taken production traffic; I have no professional Go experience. Latency instrumentation (/metrics, TTFT) isn't attached yet, which is why no performance figures appear here.
A Korean–Japanese–Chinese hanzi comparison platform over a 10,000-character database — compare readings and meanings across three languages, with automatic "false friends" detection for look-alike characters that mean different things. Live with real users.
A mobile-web (PWA) platform matching amateur musicians in Korea for jam sessions — hosts open sessions by region, genre, and skill level; players apply by instrument and get approved. Kakao OAuth, notifications, and song/venue search built in, with a four-layer test suite.