Template Method

Put behavioral differences in code, and let the domain owner write that code.

Résumé bullet

Designed a Template Method + dynamic-factory architecture so regional teams override site-specific logic over a shared core workflow, scaling to 8 sites with no added central headcount.

Deep-dive
  • Same screen, 8 sites, totally different workflows
  • Config can't express call order; that's code
  • Strategy pattern = hundreds of registrations
  • Chose inheritance: child overrides, falls back to parent
  • Delegated child classes to domain owners; reviewed PRs
  • Bug-fix turnaround days -> under a day; no central headcount added
Code
// parent = shared workflow; child overrides only the diff (Template Method)
class InboundReceipt {                 // parent (core, usable as the default)
    void confirm() { /* shared steps */ }
    void inspect() { /* default: no-op */ }
}
class ChinaPlant extends InboundReceipt {
    @Override void inspect() { qms.sendRequest(payload); }   // site-specific
}

// runtime: use the child if one is registered, else the parent
InboundReceipt screen = registry.getOrDefault(plantId, new InboundReceipt());
screen.confirm();   // zero registration boilerplate
Say it (English)
  • ·Same screen, completely different workflows per factory.
  • ·Config handles values; workflow differences need code.
  • ·Parent holds the shared flow, child overrides only the diff.
  • ·I delegated child classes to the people who knew the domain.
  • ·I reviewed and merged the PRs -- that's where I added value.
Why it matters — Opendoor / as an engineer
  • Opendoor: per-market rules over a shared core, without forking.
  • Proves: extensibility (Template Method / Strategy) + multi-tenant scaling.
  • Use it for 'how do regions/markets differ' design.
Trade-offs (what you give up)
  • Inheritance hard-couples children to the parent's call order; a core refactor silently breaks all 8 subclasses.
  • Variant behavior lives in deployed code, so every plant tweak needs a build/deploy cycle, not a config push.
  • Single inheritance means a plant sharing two orthogonal traits can't mix in both; you copy logic or deepen the tree.
  • Override-vs-core-contract is a convention, not enforced; nothing stops a child from breaking a shared invariant.
Push it further
  • Replace announce-on-change with consumer-driven contract tests per plant, gated in CI so parent changes fail fast.
  • Pull stable, low-churn variations into config/feature flags; keep only true workflow forks in code.
  • Swap deep inheritance for composition: inject per-step strategies so plants compose orthogonal behaviors without a tree.
  • Make the 'safe-to-override' contract explicit via abstract template hooks plus sealed core methods, enforced by the compiler.

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

같은 화면 하나를 공장 8곳이 쓴다. 그런데 공장마다 일하는 순서가 완전히 다르다. 어떻게 하나로 만들면서 각 공장의 차이를 담을까?
  1. 1

    화면은 똑같이 생겼는데, 공장 8곳이 일하는 방식이 다 다르다. 어떤 곳은 검사를 하고, 어떤 곳은 건너뛴다.

    class InboundReceipt {          // one shared screen, all 8 sites
      confirm() { /* shared steps */ }
      inspect() { /* default: no-op */ }
    }
    🗣 영어로 말해

    Same screen, but each of the eight factories has a completely different workflow.

    checking microphone…

  2. 2

    처음엔 설정 파일로 해결하려 했다. 하지만 설정은 '값'만 바꿀 수 있다. '어떤 순서로, 어떤 조건에서 무엇을 한다' 같은 흐름은 설정으로 못 적는다. 그건 코드다.

    ⚖️ Trade-off: 설정은 숫자나 켜고 끄기 같은 값만 담는다. 일하는 순서 자체는 못 담는다.

    ✅ Fix: 그래서 흐름의 차이는 설정이 아니라 코드로 표현하기로 했다.

    🔧 도구:config / feature flag (값만 — 여기선 부족)

    // config can only carry values, not call order
    config = { inspectThreshold: 50, autoConfirm: true };
    // it cannot express "run inspect() before confirm() only at China"
    🗣 영어로 말해

    Config can only change values, but workflow differences need real code.

    checking microphone…

  3. 3

    다음 후보는 strategy 패턴. 그런데 화면마다, 단계마다 '이건 이 공장 거'라고 일일이 등록해야 한다. 공장 8곳 x 화면 수십 개 = 등록만 수백 번. 너무 번거롭다.

    ⚖️ Trade-off: strategy는 유연하지만, 경우마다 손으로 등록해야 해서 등록 코드가 산더미가 된다.

    ✅ Fix: 같은 유연함을 등록 없이 얻을 방법을 찾았다 — 상속.

    🔧 도구:Strategy pattern (등록 보일러플레이트 과다 — 여기선 X)

    // strategy: explicit per-method registration, x8 sites x dozens of screens
    registry.register("china", "inspect", new ChinaInspect());
    registry.register("china", "confirm", new ChinaConfirm());
    // ...hundreds of these -> avoided
    🗣 영어로 말해

    The strategy pattern needs hundreds of manual registrations, so I avoided it.

    checking microphone…

  4. 4

    선택: 상속. 부모가 '공통 흐름' 전부를 갖고, 자식 공장은 '다른 부분만' 덮어쓴다. 안 덮어쓰면 자동으로 부모 것을 쓴다. 등록할 게 하나도 없다.

    ⚖️ Trade-off: 상속은 자식이 부모의 흐름·순서에 딱 붙어 있다. 부모(공통 코드)를 바꾸면 8개 자식이 조용히 다 깨질 수 있다.

    ✅ Fix: 그래서 '바꿔도 되는 부분'과 '건드리면 안 되는 핵심'을 문서로 나누고, 부모를 고치면 모두에게 알리고, 모든 공장 버전을 한 번에 테스트했다.

    🔧 도구:상속(override + 부모 fallback)통합 테스트(전 공장 버전)

    class ChinaPlant extends InboundReceipt {
      inspect() { qms.sendRequest(this.payload); } // override only the diff
    }
    // confirm() not overridden -> falls back to parent's shared flow
    🗣 영어로 말해

    The parent holds the shared flow, and each child overrides only the difference.

    checking microphone…

  5. 5

    또 다른 비용: 흐름 차이가 '배포되는 코드'에 들어 있어서, 공장 하나만 살짝 바꿔도 빌드·배포를 다시 해야 한다. 설정처럼 즉시 못 바꾼다.

    ⚖️ Trade-off: 차이가 코드 안에 있어 작은 변경도 매번 빌드·배포가 필요하다.

    ✅ Fix: 잘 안 바뀌는 차이는 설정으로 빼고, 진짜 흐름이 갈라지는 것만 코드로 남기면 배포 부담이 준다.

    🔧 도구:config/feature flag(안정적인 차이용)

    🗣 영어로 말해

    The variant logic lives in deployed code, so each tweak needs a build and deploy.

    checking microphone…

  6. 6

    핵심 결정: 각 공장 자식 코드를 그 공장 도메인을 제일 잘 아는 현지 개발자에게 맡겼다. 나는 부모(공통)를 지키고, 그들의 PR을 리뷰하고 합쳤다.

    ⚖️ Trade-off: 여러 사람이 자식을 건드리면, 누군가 핵심 약속을 깰 수 있다. 막아주는 컴파일러 강제는 없고 약속(관례)뿐이다.

    ✅ Fix: 내가 모든 PR을 직접 리뷰하는 게 안전망이었다 — 거기서 내 가치가 나왔다.

    🔧 도구:PR 리뷰현지 개발자 위임Postman 가이드(기기 없이 API 직접 테스트)

    // each child class owned by that factory's domain dev, merged via PR review
    class HungaryPlant extends InboundReceipt {
      confirm() { /* Hungary-specific flow */ }
    }
    🗣 영어로 말해

    I delegated each child class to the people who knew that factory's domain best.

    checking microphone…

  7. 7

    결과: 버그 고치는 시간이 며칠에서 하루 안쪽으로 줄었다. 가운데 팀 사람을 늘리지 않고도 됐다 — 차이를 아는 사람이 직접 그 부분만 고쳤기 때문.

    ⚖️ Trade-off: 공장이 수천 개로 늘면 이 방식(상속 트리)은 한계가 온다. 트리가 너무 깊고 복잡해진다.

    ✅ Fix: 그 규모에선 feature-flag 서비스나 strategy 등록소로 간다 — 하지만 '차이를 떼어낸다'는 원칙 자체는 그대로다.

    // use the child if one is registered, else the parent
    InboundReceipt screen = registry.getOrDefault(plantId, new InboundReceipt());
    screen.confirm();   // zero registration boilerplate
    🗣 영어로 말해

    Bug-fix turnaround dropped from days to under a day, with no added central headcount.

    checking microphone…

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