Put behavioral differences in code, and let the domain owner write that code.
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.
- 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
// 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- ·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.
- 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.
- 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.
- 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 (한국어로 이해 → 영어로 말하기)
- 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
처음엔 설정 파일로 해결하려 했다. 하지만 설정은 '값'만 바꿀 수 있다. '어떤 순서로, 어떤 조건에서 무엇을 한다' 같은 흐름은 설정으로 못 적는다. 그건 코드다.
⚖️ 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
다음 후보는 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
선택: 상속. 부모가 '공통 흐름' 전부를 갖고, 자식 공장은 '다른 부분만' 덮어쓴다. 안 덮어쓰면 자동으로 부모 것을 쓴다. 등록할 게 하나도 없다.
⚖️ 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
또 다른 비용: 흐름 차이가 '배포되는 코드'에 들어 있어서, 공장 하나만 살짝 바꿔도 빌드·배포를 다시 해야 한다. 설정처럼 즉시 못 바꾼다.
⚖️ Trade-off: 차이가 코드 안에 있어 작은 변경도 매번 빌드·배포가 필요하다.
✅ Fix: 잘 안 바뀌는 차이는 설정으로 빼고, 진짜 흐름이 갈라지는 것만 코드로 남기면 배포 부담이 준다.
🔧 도구:config/feature flag(안정적인 차이용)
🗣 영어로 말해“The variant logic lives in deployed code, so each tweak needs a build and deploy.”
checking microphone…
- 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
결과: 버그 고치는 시간이 며칠에서 하루 안쪽으로 줄었다. 가운데 팀 사람을 늘리지 않고도 됐다 — 차이를 아는 사람이 직접 그 부분만 고쳤기 때문.
⚖️ 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단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.