Shared validation

Shared logic belongs in one layer, never duplicated per surface.

Résumé bullet

Unified PC/mobile validation into one shared function reading fresh DB state; cross-surface mismatch bugs (~20% of changes) went to zero.

Deep-dive
  • Validation lived in UI event handlers, read grid cells
  • PC/mobile couldn't share — nothing to share
  • ~20% of feature changes shipped cross-surface mismatch
  • Fix: one shared function, read fresh DB state
  • My boilerplate + specialist's rules; phased rollout
  • Result: that bug class to zero; identical results
Code
// Before: logic trapped in the desktop UI handler
grid.addCellChangeListener(e -> {
    if (grid.cell("qty") > grid.cell("max"))   // stale UI state
        block();
});

// After: one entry point, fresh DB, both surfaces call it
ValidationResult validate(LotId id) {
    Lot row = db.getLot(id);          // fresh state, not the UI
    return rules.check(row);          // the specialist's business rules
}
// PC and mobile: same signature, same input, same output
Say it (English)
  • ·Validation logic wasn't in the business layer — it was inside the UI screens.
  • ·There was nothing to share; the logic only lived in the PC UI.
  • ·Highest standards meant fixing the root, not the symptom.
  • ·My boilerplate plus his rules — each did what we knew best.
  • ·I'd be first name on the post-mortem. I accepted that.
Why it matters — Opendoor / as an engineer
  • Opendoor: validation in one shared layer reading fresh state, not copied per client.
  • Proves: single-source correctness, no duplicated rules.
  • Use it when client-vs-server or duplicated validation comes up.
Trade-offs (what you give up)
  • A shared library still forces every consumer to redeploy on a rule change.
  • Reading fresh DB on each validate adds a round trip and read load.
  • No client pre-check means worse latency and no offline validation.
  • A buggy rule ships to all surfaces at once via the shared lib.
Push it further
  • Promote to a validation service so rules update without consumer redeploys.
  • Cache hot lot state with short TTL to cut DB round trips.
  • Add contract tests so each surface verifies the shared signature.
  • Version rules and roll out behind flags to bound blast radius.

Understand it step by step (한국어로 이해 → 영어로 말하기)

같은 검사 규칙(예: 수량이 최대치를 넘으면 막기)이 PC와 모바일 두 화면에 다 필요했다. 그런데 그 규칙이 공통 코드가 아니라 PC 화면 안에만 들어가 있었다. 그래서 모바일은 따라할 게 없었고, 두 화면 결과가 서로 달라졌다.
  1. 1

    검사 규칙이 화면(UI) 코드 안에 들어가 있었다. PC 화면의 표(grid) 칸 값을 직접 읽어서 '이게 맞나' 판단했다.

    🔧 도구:UI event handler (grid.CellChanged)

    // Correct: the UI handler stays thin and delegates to the shared layer
    grid.addCellChangeListener(e -> {
        ValidationResult result = validate(lotId);   // logic lives outside the UI
        if (!result.ok()) block();
    });
    🗣 영어로 말해

    The validation logic lived inside the UI screen, not in a shared layer.

    checking microphone…

  2. 2

    문제: 모바일에서 같은 규칙을 쓰고 싶어도, 가져다 쓸 공통 코드가 아예 없었다. 규칙이 PC 화면 안에만 있으니까.

    ⚖️ Trade-off: 공통 함수가 없으니 모바일은 규칙을 또 새로 짜야 한다 → 같은 규칙이 두 곳에 따로 생김.

    // One shared function both surfaces can call -- something to share
    ValidationResult Validate(LotId id) {
      var row = db.GetLot(id);
      return rules.Check(row);
    }
    🗣 영어로 말해

    There was nothing to share, so PC and mobile drifted apart.

    checking microphone…

  3. 3

    그래서 같은 상황인데 PC와 모바일 결과가 달랐다. 기능을 바꿀 때마다 한쪽만 고쳐져서, 변경의 약 20%가 화면끼리 안 맞는 버그로 나갔다.

    ⚖️ Trade-off: 두 곳을 따로 고치면 한 곳을 빼먹기 쉽다 → 작업자는 두 화면 다 못 믿게 됨.

    🗣 영어로 말해

    About one in five changes shipped a mismatch between the two screens.

    checking microphone…

  4. 4

    해결: 규칙을 화면 밖, 공통 함수 한 곳으로 빼냈다. 그리고 화면 표 칸이 아니라 DB에서 지금 값을 새로 읽어서 판단하게 했다. PC도 모바일도 같은 함수를 부른다.

    ⚖️ Trade-off: 검사할 때마다 DB를 한 번 더 읽으니 살짝 느려지고 DB 부하가 는다.

    ✅ Fix: 화면에 떠 있던 옛날 값(stale)이 아니라 항상 최신 값으로 판단하니 정확해진다 → 그 한 번의 왕복은 정확성 값으로 받아들임.

    🔧 도구:shared validation librarysingle entry point Validate(id)fresh DB read

    ValidationResult Validate(LotId id) {
      var row = db.GetLot(id);   // fresh DB state, not stale UI cells
      return rules.Check(row);   // specialist's business rules
    }
    // PC and mobile call the same Validate(id)
    🗣 영어로 말해

    I moved the rule into one shared function that reads fresh state from the DB.

    checking microphone…

  5. 5

    역할을 나눴다. 나는 뼈대(공통 호출 구조, 보일러플레이트)를 짜고, 업무 규칙은 그 분야를 제일 잘 아는 전문가가 채웠다. 그리고 한 화면부터 천천히 켰다.

    ⚖️ Trade-off: 공통 라이브러리라 규칙 하나만 바꿔도 그걸 쓰는 모든 화면을 다시 배포해야 한다. 또 규칙에 버그가 있으면 모든 화면에 한꺼번에 퍼진다.

    ✅ Fix: 단위·통합 테스트로 받쳐 두고, 한 화면씩 단계적으로 켜서 문제가 터져도 범위를 작게(blast radius) 막았다.

    🔧 도구:unit + integration testsphased rollout

    // Boilerplate signature; specialist fills rules.Check; tested + phased
    [Test] void rejects_qty_over_max() {
      var result = Validate(lotId);   // same entry point both surfaces use
      Assert.False(result.Ok);
    }
    🗣 영어로 말해

    I wrote the boilerplate, the specialist wrote the rules, and we rolled it out one screen at a time.

    checking microphone…

  6. 6

    결과: 화면끼리 안 맞던 그 버그 종류가 0이 됐다. 같은 입력이면 PC든 모바일이든 똑같은 결과가 나온다.

    ⚖️ Trade-off: 지금 방식은 '라이브러리'라 규칙 바꿀 때마다 쓰는 쪽이 전부 업그레이드해야 한다.

    ✅ Fix: 다음엔 라이브러리 말고 검사 '서비스'로 만들면, 한 곳만 고쳐도 쓰는 쪽은 그대로 둘 수 있다.

    🔧 도구:validation service (next step)contract testsversioned rules behind flags

    🗣 영어로 말해

    That whole class of mismatch bugs went to zero — same input, same result everywhere.

    checking microphone…

  7. 7

    한 줄 원칙: 같은 규칙이 두 화면에 다 필요하면, 그건 화면이 아니라 공통 층 한 곳에 있어야 한다. '화면에 있다'가 답이면 설계가 이미 틀린 거다.

    🗣 영어로 말해

    If a rule applies on two surfaces, it belongs in one shared layer, never the UI.

    checking microphone…

7단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.