Send identifiers, not state — let the server derive the rest from its source of truth.
Resolved HTTP 413 errors on mobile WMS scans without infra changes using PK-only payloads with fresh-state retrieval, handling 50 LOTs per batch (validated to 100) and keeping inventory accurate.
- Problem: batch save hit HTTP 413 past ~20 items
- Refused easy fix: raising shared-server limit risks others
- Root cause: client sent 10+ redundant columns per item
- Second cause: scan-time data went stale before save
- Fix: send PK only, server re-fetches fresh from DB
- Result: ~95% smaller payload, 413s gone, 20->50
// Before: client ships full UI state (10+ cols/item) -> 413 Payload Too Large
// POST /save { lots: [{ plant, lot, qty, status, loc, ... }] }
// After: client ships only identifiers (PK only)
// POST /save { lots: [{ plant, lot }] }
// Server derives the rest, fresh, at save time
List<Row> save(List<Key> keys) {
List<Row> rows = jdbc.query(MASTER_QUERY, keys); // indexed lookup, sub-ms
return persist(rows);
}- ·The fast fix was raising the server limit. I refused it.
- ·It was shared infra. My fix would become everyone's risk.
- ·I found two problems, not one: payload size and stale data.
- ·Client now sends only primary keys. Server reads fresh from the DB.
- ·Payload dropped ~95%, 413s stopped, batch went 20 to 50.
- Opendoor: bulk actions on offers/listings send IDs; the server loads current state, not the client's snapshot.
- Proves: API-contract judgment, source-of-truth over client state, frugality.
- Use it when the client sends data the server already owns, or staleness comes up.
- Adds N extra indexed DB reads per save; sub-ms now but scales with batch size.
- Server-derived master data silently overrides what the scanner saw; no diff surfaced to the user.
- Couples save latency and correctness to MASTER_QUERY's index health; a missing index regresses every save.
- Dormant old dispatch handler kept two weeks doubles the contract surface to test.
- Batch the per-item lookups into one WHERE (plant,lot) IN (...) query to avoid N round-trips.
- Add idempotency: dedupe keys server-side and make persist upsert so retries are safe.
- Send a scan-time hash/version per PK; reject or flag on optimistic-lock mismatch.
- Contract test the PK-only schema in CI to catch clients regressing to full-state payloads.
Understand it step by step (한국어로 이해 → 영어로 말하기)
- 1
현장에서 항목을 여러 개 스캔해서 한 번에 저장한다. 그런데 20개쯤 넘으면 서버가 거부한다(HTTP 413, '요청이 너무 큼').
// Batch save shipping full rows fails once the body grows past the server cap // POST /save -> HTTP 413 Payload Too Large List<LotDto> lots = grid.getRows(); // ~20+ items, each fat restClient.post("/save", new SaveRequest(lots));🗣 영어로 말해“A batch save started failing once it had more than about twenty items.”
checking microphone…
- 2
가장 쉬운 길은 서버의 크기 제한을 그냥 올리는 것이다. 근데 나는 안 했다.
⚖️ Trade-off: 그 서버는 여러 서비스가 같이 쓰는 공용이다. 내가 제한을 올리면 다른 서비스도 영향을 받는다(메모리 압박 등).
✅ Fix: 내 편의 때문에 남들 위험까지 떠안기는 건 아니다 → 제한을 안 올리고 다른 방법을 찾았다.
🔧 도구:IIS maxRequestLength / maxAllowedContentLength
<!-- web.config: the shared-server knob I refused to raise --> <httpRuntime maxRequestLength="4096" /> <requestLimits maxAllowedContentLength="4194304" /> <!-- raising this is global: every co-hosted service pays the memory cost -->
🗣 영어로 말해“The fast fix was raising the server limit, but I refused it.”
checking microphone…
- 3
진짜 원인은 두 개였다. 하나: 항목 하나마다 화면에 있는 컬럼 10개 넘게 다 같이 보냄 → 그래서 요청이 너무 컸다.
⚖️ Trade-off: 화면 상태를 통째로 보내면 보내는 양이 항목 수만큼 커진다.
// Bloated: one item carries 10+ columns of UI state class LotDto { String plant, lot; int qty; String status, loc, /* ...10+ */; } // Correct: ship only what identifies the row class LotKey { String plant; String lot; }🗣 영어로 말해“First problem: the client sent ten-plus columns per item, so the payload was huge.”
checking microphone…
- 4
둘: 스캔한 시점과 저장하는 시점 사이에 시간이 떠서, 화면에 들고 있던 값이 이미 옛날 값(stale)이 될 수 있었다.
⚖️ Trade-off: 화면 값을 그대로 저장하면 몇 시간 전 값을 저장할 수도 있다.
🗣 영어로 말해“Second problem: the scanned data could already be stale by save time.”
checking microphone…
- 5
해결: 클라이언트는 식별자(기본키)만 보낸다. 예전엔 {plant, lot, qty, status, loc, ...} 다 보냈는데, 이제 {plant, lot}만 보낸다.
⚖️ Trade-off: 서버가 나머지 값을 직접 알아내야 한다 → 저장할 때 DB를 한 번 더 읽는 일이 생긴다.
✅ Fix: 그 컬럼들은 plant+lot로 색인된 조회라 1ms도 안 걸린다. 저장 자체가 50~100ms라 그 정도는 그냥 노이즈 수준 → 무시 가능.
🔧 도구:primary key only payloadindexed lookup (plant + lot)WHERE (plant,lot) IN (...) 배치 조회
-- Client sends primary keys only; server re-fetches the rest in one batch SELECT plant, lot, qty, status, loc FROM lot WHERE (plant, lot) IN ((:p1,:l1), (:p2,:l2), /* ... */); -- composite index on (plant, lot) -> sub-ms indexed lookup
🗣 영어로 말해“The client now sends only primary keys; the server reads the rest fresh from the DB.”
checking microphone…
- 6
키만 받으면 서버가 그때그때 '진짜 최신 값'을 DB에서 다시 읽는다. 두 문제(크기 + 옛날 값)가 한 번에 풀린다.
⚖️ Trade-off: 스캐너가 본 값과 DB 값이 다르면, 서버 값으로 조용히 덮어쓴다. 사용자에겐 그 차이를 안 보여준다.
✅ Fix: 그 컬럼들은 시스템이 주인인 마스터 데이터라, 옛 스캔 값보다 새 DB 값이 더 맞다. 사람이 직접 입력한 칸은 그대로 보내서 안 건드린다.
🔧 도구:source-of-truth re-fetchsystem-owned master data
// Server derives everything from its source of truth at save time List<LotRow> save(List<LotKey> keys) { List<LotRow> fresh = lotRepository.findByKeys(keys); // fresh master data return lotRepository.persist(fresh); // never trust UI snapshot }🗣 영어로 말해“The server re-derives the rest from its source of truth, so both problems vanish.”
checking microphone…
- 7
결과: 보내는 양이 약 95% 줄고, 413 에러가 사라지고, 한 번에 저장 가능한 개수가 20개 → 50개로 늘었다.
🗣 영어로 말해“Payload dropped about ninety-five percent, the errors stopped, and the batch went from twenty to fifty.”
checking microphone…
- 8
그래서 내 API 규칙이 생겼다: '클라이언트가 보내는 데이터를 서버가 스스로 알아낼 수 있나?' 그렇다면 설계가 틀린 거다.
⚖️ Trade-off: 롤백 대비도 했다: 옛 처리기를 2주간 꺼두지 않고 살려둬서, 문제 생기면 몇 분 만에 되돌릴 수 있게 했다.
✅ Fix: 단, 옛 처리기를 살려두면 테스트할 계약(contract)이 두 개로 늘어난다 → 2주만 두고 정리.
🔧 도구:legacy dispatch handler (dormant fallback)contract test in CI
// Keep the old handler dormant for fast rollback; a contract test pins the new shape @Test void saveAcceptsPkOnlyPayload() { var req = new SaveRequest(List.of(new LotKey("P1", "L1"))); assertEquals(200, client.post("/save", req).status()); }🗣 영어로 말해“My rule now: if the server can derive what the client sends, the design is wrong.”
checking microphone…
8단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.