SC
/
← Back to projects

Apr 2026 – Present

HowMuch

A group expense app that manages receipt OCR, equal and item-based splits, payment requests, and payment status.

JavaSpring BootPostgreSQLRedisDockerFCMReact NativeTypeScriptExpoCodex
HowMuch cover

Overview

HowMuch reduces the effort of calculating everyone’s share and checking payments after a group gathering. Receipt OCR and equal or item-based splits simplify expense entry and allocation. Payment requests, outstanding balances, and each participant’s payment status are managed in one place.

  • Period: April 2026 – Present
  • Team: 7 people · 4 frontend, 2 backend (including me), 1 AI
  • Role: Backend and frontend development
  • iOS: TestFlight testing in progress

Contributions

  • Settlement and group APIs; equal and item-based expense allocation
  • Lock optimization: read p95 from 14.7s to 0.5s
  • Asynchronous Outbox processing: settlement API p50 from 15.1s to 21.6ms
  • Redis authentication and session concurrency control
  • Outstanding settlement widget API and monthly recap
  • Frontend push notification delivery and improvements

Lock Wait Limits and Transaction Separation

Problem

Under a forced database lock, ten writes to one settlement delayed unrelated reads. PostgreSQL showed ten lock waiters, while HikariCP reported ten active connections and zero idle connections: waiting requests had occupied the entire pool.

Writes and response queries shared a transaction, and external CODEF calls could also extend connection occupancy.

Approach

  • Set lock_timeout to 500ms; enlarging the pool alone would not resolve prolonged occupancy.
  • End the transaction and return HTTP 503 when lock acquisition fails.
  • Save submissions, allocated amounts, and settlement state together; query the response in a separate transaction after commit.
  • Standardize lock order as Account → Settlement for operations updating both.

Separated write and response transactions using a shared connection pool; original diagram from V13

Results

  • Unrelated read p95: 14,715ms → 405–504ms, approximately 97% lower
  • Unrelated read failures during long locks: 5/10 → 0/10; 10/10 writes succeeded after the lock was released
  • Zero deadlocks in 100 concurrent account deletion / recipient-account selection runs
  • Injected save-stage exceptions rolled back submissions, allocations, and settlement state together

Blog — PostgreSQL Row Locks and HikariCP · PR #210

Transactional Outbox for Response Time and Notification Recovery

Problem

The request thread sent FCM notifications sequentially in afterCommit, so external delays accumulated with participant count. Notification tasks were not persisted, preventing retries and recovery after process termination.

Approach

A simple asynchronous call would still lose in-memory tasks on termination. I chose a Transactional Outbox to persist the settlement and notification intent together.

  • Return after the Outbox commit; a separate worker sends FCM notifications outside database transactions.
  • Apply exponential backoff and up to five attempts for temporary errors; recover interrupted work.
  • Handle concurrent claims with FOR UPDATE SKIP LOCKED.
  • Replace batch claims with single-event claims immediately before sending to reduce lease expiry while waiting.

Settlement and Outbox commit followed by independent worker delivery; original diagram from V13

Results and Limitations

  • With 30 participants and a 500ms Fake Sender delay, API p50 fell from 15,138.7ms to 21.6ms, approximately 99.86% lower.
  • When every third send failed, all 30 events succeeded after retries.
  • An Outbox persistence conflict rolled back all related settlement data.
  • Terminating the worker immediately after external acceptance produced 30 completed events and 31 external acceptances, demonstrating possible duplicates under at-least-once delivery.

These figures measure API response time under the Fake Sender experiment, separately from real FCM transmission or total notification delivery time.

Blog — Transactional Outbox · PR #257

Atomic Session Replacement in Redis

Problem

Separate session validation and persistence allowed a delayed refresh to overwrite a logout or a new login. Concurrent requests could also validate the same refresh token and both succeed.

Approach

Redis Lua checks the current session’s TTL and whether its data still matches the validated session, then replaces it atomically only when both conditions hold. Deleted, expired, or changed sessions reject refreshes. Absolute expiry prevents processing delays from extending session lifetime.

Eight concurrent refreshes with the same token: one success, seven rejections.

Race conditionBeforeAfter
Logout during refreshDeleted session restoredLogged-out state preserved
New login during refreshNew session overwrittenNew session preserved
Expiry after validationExpired session restoredRefresh rejected

PR #316

Lessons Learned

LLM Agent Development Workflow

I organized six agents for analysis, planning, implementation, QA, review, and Git management, with an orchestrator. The workflow connects issues, specifications, approval, implementation, tests, PR creation, and teammate review requests. Problems found in QA or review feed back into fixes and verification.

Verifying Performance and Failure Behavior

I traced bottlenecks caused by database locks and synchronous notifications, compared response times before and after changes, and reproduced failures to check consistency and recovery as well as speed.

Agreeing on Responsibilities and Deadlines

Different circumstances and priorities across teammates taught me to agree on each person’s scope and deadlines rather than expect identical levels of involvement.

Upcoming Service Presentation

I have an opportunity to introduce the service through a booth and presentation at the 2026 Korea ICT Convergence Expo.