Home Pricing / Valuation Pipeline

System Design

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

Opendoor는 집주인이 주소를 넣으면 "우리가 이 집을 얼마에 살게요"라는 가격(오퍼)을 보여줘야 한다. 이 가격은 기준값 + 리노베이션 + 리스크 + 시장상황을 합쳐서 나오는데, ML 모델과 여러 외부 데이터(비교 매물, 시세)가 필요해서 계산이 무겁다. 핵심은 무거운 가격 계산을 사용자에게 보이는 오퍼 상태와 분리하고, 모든 가격을 버전으로 남겨서 "왜 이 가격이 나왔는지" 항상 설명할 수 있게 만드는 것이다.
  1. 1

    요구사항부터 명확히 한다. 기능: (1) 집주인이 주소를 넣으면 추정 가격을 보여준다. (2) 정식 오퍼를 생성하면 확정 가격을 붙인다. (3) 모든 가격은 컴포넌트별 내역(base + renovation + risk + market)으로 설명 가능해야 한다. 비기능: 가격은 돈이라 정확성과 설명가능성이 최우선이다. 추정 가격은 빠르면 좋지만(저지연), 확정 오퍼 가격은 조금 느려도 되니 정확성/일관성을 택한다. 같은 입력 + 같은 모델 버전이면 같은 가격이 나와야 한다(재현성).

    ⚖️ Trade-off: 즉석 추정은 가용성/저지연 쪽, 확정 오퍼 가격은 강한 일관성/감사 쪽으로 나눈다. 하나의 SLA로 묶으면 둘 다 망친다.

    🔧 도구:explainability/auditstrong vs eventual consistency

    🗣 영어로 말해

    Pricing is money, so I prioritize correctness and explainability over latency for binding offers.

    checking microphone…

  2. 2

    규모를 대충 잡는다. 미국 전체 약 1억 4천만 가구, 하지만 매일 가격을 다시 매기는 건 활성 시장 + 관심 매물 위주다. 가정: 하루 추정 요청 100만 건 → 평균 약 12 QPS, 피크 약 50 QPS (낮 시간 몰림). 정식 오퍼 생성은 하루 5만 건 정도 → 1 QPS 미만. 읽기:쓰기 = 압도적 읽기 (가격은 한 번 계산하고 여러 번 본다, 대략 100:1). 가격 레코드 한 건 약 2KB(내역 + 모델 버전 + 피처), 하루 100만 건이면 약 2GB/일, 1년이면 약 700GB → 버전을 다 보관해도 한 클러스터로 감당.

    ⚖️ Trade-off: QPS가 낮아서(피크 50) 즉석 ML 추론을 매 요청마다 동기로 돌릴 수도 있지만, 모델 추론은 느리고(수백 ms~초) 비싸니 결과를 캐시/사전계산하는 게 이득이다.

    ✅ Fix: 추정 요청은 캐시 우선(인기 우편번호는 사전계산), 캐시 미스만 비동기 워커로 모델 추론.

    🔧 도구:back-of-enveloperead-heavy 100:1

    🗣 영어로 말해

    About fifty peak QPS, heavily read-skewed, so I cache priced results instead of recomputing per request.

    checking microphone…

  3. 3

    API와 데이터 모델. 엔드포인트: POST /valuations (주소/집 특성 넣고 추정 요청, 비동기면 valuationId 반환) · GET /valuations/{id} (가격 + 내역 조회) · POST /offers (오퍼 생성, 현재 valuation을 스냅샷으로 붙임). 핵심: 가격 테이블은 불변(immutable)으로 버전마다 새 행을 쌓는다 — 절대 덮어쓰지 않는다. 파티션 키는 home_id, 정렬 키는 created_at(또는 version)으로 두면 '이 집의 가격 히스토리'를 한 번에 읽는다. breakdown은 JSON으로 같이 저장해 설명가능성을 보장.

    ⚖️ Trade-off: 가격 행을 덮어쓰면 저장은 작아지지만 '예전엔 왜 이 가격이었나'를 못 따진다 — 오퍼 분쟁/감사에서 치명적.

    ✅ Fix: append-only로 모든 버전 보관 + 오퍼는 valuation_id로 특정 버전을 스냅샷 → 모델이 바뀌어도 오퍼가 본 가격은 고정.

    🔧 도구:Postgresprimary key (home_id, created_at)append-only / immutable rowsJSONB breakdown

    POST /valuations { homeId, features } -> 202 { valuationId }
    GET  /valuations/{id} -> { price, modelVersion, breakdown:{base,renovation,risk,market} }
    
    -- valuations (append-only, Postgres)
      home_id        TEXT        -- group key
      created_at     TIMESTAMPTZ -- newest-first read
      valuation_id   UUID
      price_cents    BIGINT
      model_version  TEXT
      breakdown      JSONB
      PRIMARY KEY (home_id, created_at)
    🗣 영어로 말해

    The valuation table is append-only and an offer snapshots a specific valuation id, so its price never silently changes.

    checking microphone…

  4. 4

    상위 설계, 요청 흐름. 클라이언트 → 로드밸런서 → Valuation API → 먼저 Redis 캐시 확인(home_id+modelVersion 키). 히트면 즉시 반환. 미스면 가격 레코드를 PENDING으로 만들고 Kafka에 가격계산 작업을 넣고 valuationId를 바로 돌려준다(202). Pricing Worker가 큐에서 꺼내 → 외부 데이터(비교 매물, 시세 API) 모으고 → ML 추론 서버 호출 → base+renovation+risk+market 합산 → 결과를 가격 테이블에 READY로 쓰고 캐시 채움. 오퍼 서비스는 이 가격 파이프라인과 완전히 분리 — 오퍼는 사용자에게 보이는 상태(CREATED→SENT→ACCEPTED)만 책임지고, 가격은 valuation_id로 가져온다.

    ⚖️ Trade-off: 동기로 ML을 부르면 코드가 단순하지만 추론 서버가 느려지거나 죽으면 사용자 요청 전체가 막힌다.

    ✅ Fix: PENDING 행 + 큐 + 워커로 무거운 계산을 비동기로 빼서, 사용자 레코드는 먼저 안전하게 커밋되고 가격은 뒤따라 채워진다.

    🔧 도구:Redis cacheKafkaasync workerML inference serviceoutbox/queue

    🗣 영어로 말해

    On a cache miss I commit a PENDING record, enqueue the compute, and a worker fills in the price asynchronously.

    checking microphone…

  5. 5

    딥다이브 1 — 핫 키와 캐시 스탬피드. 인기 지역(예: LA 한 우편번호)의 같은 집/같은 비교군에 추정 요청이 동시에 몰리면, 캐시가 비어 있는 순간 수십 요청이 전부 '내가 계산해야지' 하고 같은 무거운 ML 추론을 N번 돌린다 → 추론 서버 폭발. 해결: 캐시 미스 시 home_id+modelVersion에 분산 락(또는 single-flight)을 걸어 첫 요청만 계산하게 하고, 나머지는 그 결과를 기다리거나 PENDING valuationId만 받아간다. 인기 우편번호는 야간 배치로 사전계산해 캐시를 미리 데워 둔다. 모델 버전을 캐시 키에 포함해, 새 모델 배포 시 자동으로 키가 갈려 stale 가격이 안 나간다.

    ⚖️ Trade-off: 락/single-flight는 코드와 조율 지점이 늘고, 락 주인이 죽으면 대기자가 멈출 수 있다.

    ✅ Fix: 락에 lease(TTL)를 걸어 주인이 죽어도 자동 해제 + 계산은 멱등이라 재시도 안전. 사전계산으로 핫 키는 애초에 미스가 안 나게.

    🔧 도구:distributed lock / single-flightcache stampedelease TTLprecompute/warm cacheversion in cache key

    key = `val:${homeId}:${modelVersion}`
    if (cache.has(key)) return cache.get(key)
    if (!lock.tryAcquire(key, leaseMs=30000)) return pendingValuationId // others wait
    compute(); cache.set(key, result); lock.release(key)
    🗣 영어로 말해

    On a hot key I single-flight the compute under a leased lock so ten callers don't trigger ten model runs.

    checking microphone…

  6. 6

    딥다이브 2 — 모델 버전 변경과 가격 일관성. 새 ML 모델을 배포하면 같은 집의 가격이 바뀐다. 이미 SENT된 오퍼가 새 모델로 가격이 출렁이면 사용자 신뢰가 깨지고 법적 문제까지 간다. 해결: 모델 버전을 일급 시민으로 다룬다 — 모든 가격 행에 model_version을 박고, 오퍼는 생성 시점의 valuation_id(=특정 모델 버전)를 스냅샷으로 고정한다. 새 모델은 shadow로 먼저 돌려 옛 모델과 가격을 비교(reconciliation)하고, 큰 차이가 나는 집은 사람 검토 큐로 보낸다. 배포는 카나리로 일부 지역만 새 버전 → 모니터링 후 확대.

    ⚖️ Trade-off: 버전을 다 보관하고 shadow까지 돌리면 저장/연산 비용이 늘고, 옛 버전 정리 정책이 필요하다.

    ✅ Fix: 오퍼는 스냅샷이라 모델이 바뀌어도 본 가격 불변 + shadow/카나리로 위험을 점진 노출 + 차이 큰 집만 사람 검토로 비용 한정.

    🔧 도구:model versioningshadow deploycanary by regionreconciliationhuman review queue

    🗣 영어로 말해

    An offer pins the valuation id at creation, so deploying a new model never moves a price someone already saw.

    checking microphone…

  7. 7

    트레이드오프 + 모니터링. 포기한 것: 즉석 강한 일관성 — 추정 가격은 캐시/비동기라 잠깐 stale일 수 있고 PENDING 상태가 사용자에게 보인다(저지연/가용성과 맞바꿈). 단순함도 포기 — 큐, 워커, 버전 테이블, 락이 운영 표면을 늘린다. 볼 지표: 캐시 적중률(낮아지면 추론 서버 과부하 신호), 워커 큐 깊이/지연(가격이 안 채워지는지), ML 추론 p99 지연과 에러율, shadow 모델과 현 모델의 가격 차이 분포(드리프트), PENDING→READY 전환 시간, breakdown 합이 최종가와 항상 일치하는지(설명가능성 불변식). 알림: 큐 깊이 급증, 추론 에러율 상승, 가격 드리프트 임계 초과.

    ⚖️ Trade-off: 비동기/캐시로 저지연·가용성을 얻는 대신 즉석 일관성과 운영 단순함을 내줬다.

    ✅ Fix: 지표로 약점을 감시 — 캐시 적중률, 큐 깊이, 추론 p99, 가격 드리프트, breakdown 합 일치 불변식.

    🔧 도구:cache hit ratequeue depth/lagp99 latencydrift monitoringalerts

    🗣 영어로 말해

    I'd watch cache hit rate, queue depth, inference p99, and price drift between the live and shadow models.

    checking microphone…

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

🎙 이 카드 AI랑 음성 대화 · 기록 저장