Engineering ai-driven-dev 33 min read

Booking Engine を自作する設計戦略 — 受託から商用ライセンスへ

受託で積んだ実装資産を商用ライセンスへ昇格させる 4 つの抽象化規約を公開。Phase A→D の段階設計・adapter pattern・tenantId で、dogfooding しながら破壊的変更なしに SaaS 化準備を進める。

公開 2026-05-21 森本 拓見

本記事は bateson Booking Engine の 設計判断を担うエンジニア・アーキテクト 向けに書いている。AIネイティブ組織土台上での事業ポートフォリオ・Phase 戦略・受注 funnel 設計の business 観点は、経営判断側の記事 「AIネイティブ組織土台と事業ポートフォリオ — bateson に見る Phase 戦略」 を参照。本記事では adapter pattern / tenantId / DI / headless UI 設計の中身を扱う。

bateson は single-tenant 実装で multi-tenant 化を妨げない設計 を Phase A の段階から守るための 4 抽象化規約と、それを実装する adapter / DI / headless UI 構造で構成される。本記事ではその規約の中身と、src/lib/bateson/ の実コードを順に解説する。

Key takeaway 3 点:

  1. Phase A で守るべき規約は 4 つだけ。逆向き依存禁止 / tenantId 全 API 引数化 / adapter 差し替え可能 / テンプレ注入——これが Phase B-D への破壊的変更を Phase A から閉め出す約束事だ。
  2. tenantId はシグネチャに最初から含める。Phase A では 'yakumo' 固定で構わない。重要なのはシグネチャが変わらないことで、Phase B で全 API を書き直す破壊的変更を回避できる。
  3. 4 種類の adapter(Calendar / Email / Storage / Scheduler)はそれぞれ interface で抽象化する。core ロジックは adapter の interface だけに依存し、実装の差し替えは adapter を追加するだけで済む。

bateson は現在、八雲のコーポレートサイト /contact フローで dogfooding 中だ。将来は @yakumo/bateson として独立させる予定(事実 context として記す)。

技術詳細は記事末尾 → #tech-detail にまとめている。adapter interface の実際の型定義・createBookingCore の DI 構造・hook シグネチャを確認したい読者はそちらから読むことをすすめる。


なぜ予約ツールを自作するのか — 既存 SaaS との差別化軸

予約 SaaS 市場には選択肢が揃っている。Cal.com(OSS / フル REST API)、Calendly(商用 SaaS)、TimeRex、Spir——日本語対応を含めると十分な選択肢がある。それでも八雲が bateson を自作する理由は主に 3 点だ。

第一に、AI エージェント開発組織として Booking Engine 自体を dogfooding する文脈設計。Cal.com や Calendly を導入すれば booking の adapter pattern・tenantId・headless UI といった設計判断が外部 SaaS に閉じられ、エージェント組織としての設計資産が育たない。bateson を自作することで、adapter pattern の実設計判断を八雲自身の受注フローで磨き、その資産を内製知識として蓄積できる。

第二に、将来の商用展開で必要になる顧客固有の予約フロー設計を SaaS の制約に縛られず実装するため。Multi-tenant 化以降の現場では「承認フローを組み込んだ予約」「tenant ごとに異なる空き枠ロジック」「既存 CRM への深い統合」といった要件が想定される。既製 SaaS はこれらの要件に対して拡張の限界がある。

第三に、adapter pattern を内部資産として育て、Phase A→D で段階的に事業ライン化する中期戦略。AIネイティブ組織土台上で事業ポートフォリオの 1 つとして bateson を展開していく経営判断の詳細は business 観点の記事 「AIネイティブ組織土台と事業ポートフォリオ — bateson に見る Phase 戦略」 に詳述している。

なお Cal.com / Calendly を否定する意図はない。既存 SaaS が最善解である組織は多い。八雲固有の文脈——AI エージェント開発組織として設計を dogfooding し、AIネイティブ組織土台上で事業ポートフォリオを展開する——という条件が揃ったときに初めて自作の判断が成り立つ。


marketing engine と sales engine — mcluhan と bateson が組む受注 funnel

bateson は単独で意味を持つ予約 engine ではない。八雲の受注 funnel の中で sales engine として位置付けられ、対になる marketing engine mcluhan(owned media 運用エンジン、owned media 運用エンジン mcluhan の設計)と組んで機能する。

2 engine の役割分担はシンプルだ。

  • mcluhan = marketing engine: 記事を運用し、owned media を通じて流入を作る
  • bateson = sales engine: 流入してきた読者を問い合わせ・予約・商談へ変換する

受注 funnel の流れとしては「記事(mcluhan)→ 認知 → 問い合わせ → 予約(bateson)→ 商談」という経路になる。今この記事を読んでいるのも mcluhan が運用する owned media であり、記事末尾の問い合わせフォームは bateson が運用する sales engine の入口だ。

engineering 観点では、2 engine とも adapter pattern + tenantId を共有する設計原則 で構築されている。mcluhan の記事パイプラインも bateson の予約フローも、同じ抽象化規約の上に乗っている。この設計共通性が、将来 multi-tenant 展開する際に 2 engine を一体で提供する選択肢を保つ。

事業ポートフォリオ・ROI・受注 funnel の詳細は事業判断を扱う記事 「AIネイティブ組織土台と事業ポートフォリオ — bateson に見る Phase 戦略」 に委ねる。本記事は engineering 視点で 2 engine が共有する設計原則を整理した。


Phase A → D の実装ロードマップ — single-tenant 実装で multi-tenant 化を妨げない

Phase 定義と実装ターゲット

bateson の実装は 4 Phase に分かれる。各 Phase で「何を実装するか / 何を実装しないか / 抽象化境界はどこか / 残依存は何か」を整理する。

Phase実装ターゲット抽象化レベル残依存
A. MVPsrc/lib/bateson/ 配下で single-tenant 実装、4 抽象化規約を適用adapter interface + tenantId シグネチャ確立tenantId: 'yakumo' 固定、corporate-site 内に閉じる
B. Multi-tenant リファクタstorage adapter を tenant-scoped 化、config を tenant 別分岐adapter の内部実装変更のみ(シグネチャは A から変えない)corporate-site 内、Astro 等への依存あり
C. リポジトリ抽出git filter-branchbateson/ を別リポジトリ化、npm package または git submodulesrc/lib/bateson/ が corporate-site の何も import しない状態依存ゼロ(抽出可能)
D. プロダクト化ブランド命名確定、配布形態(SaaS / OSS)確定、サインアップフローadapter と templates を外部から注入できる状態別リポジトリとして自立

Phase A が Phase B-D の前提条件になる理由

Phase A での「ちょっとした妥協」が Phase B-D の実装を壊す。具体的には:

❌ tenantId = 'yakumo' をハードコードする
  → Phase B で全関数シグネチャを変える破壊的変更が必要

❌ import { something } from '@/lib/i18n' を 1 行書く
  → Phase C のリポジトリ抽出(git filter-branch)が失敗

❌ メールテンプレに八雲のブランド文言を直埋めする
  → Phase D で別テナント対応時にテンプレ全体を書き直すことになる

4 抽象化規約はこの破壊的変更を Phase A から閉め出すための約束事だ。規約を守るコストは Phase A では数時間から数日の設計判断に相当し、守らなかった場合は Phase B-D で数週間から数ヶ月の全面的なリファクタになる。

Phase A で「実装しないこと」の設計判断

Phase A で意図的に実装しないことを明示する。

  • tenant 分離ロジック: tenantId は引数として存在するが、storage 内のデータ分岐はまだ行わない。Phase A では全データが同一領域にある
  • テナント別 config: BookingConfig は Yakumo 固定値で構わない。Phase B で分岐する拡張点を interface に含めておく
  • 外部向け API 認証: Yakumo の corporate-site から内部呼び出しするだけなので、汎用の API key 認証は不要。Phase D で追加する

「実装しない」ことを明示することで、Phase A の scope が確定し、4 抽象化規約が「何を守れば Phase B 以降で壊れないか」の最小集合として機能する。

現在の進捗(Phase A 完了相当)

2026 年 5 月時点で、bateson は Phase 3b = Phase A 完了相当まで実装が進んでいる。

  • adapter layer(Calendar / Email / Storage / Scheduler の 4 種)
  • core ロジック(createBookingCore ファクトリ + DI 構造)
  • UI hooks(useAvailability / useBooking
  • headless components(Calendar / TimeSlots / BookingFlow

API エンドポイント(/api/booking/availability/api/booking/create/api/booking/{token}/cancel/api/booking/{token}/reschedule)も実装済みで、corporate-site の /contact Step4 に統合している。


4 つの抽象化規約 — Booking Engine 設計の核

これが本稿の核心だ。Phase A の段階で守るべき 4 規約を定義する。

規約 1: corporate-site への依存禁止(core / extension 境界の設計)

src/lib/bateson/ 配下のファイルは、corporate-site のコードを一切 import しない。許可される依存は 2 種類だけだ。

  • 同パッケージ内: ../core/types など bateson ライブラリ内部の参照
  • 純粋 npm 依存: node:crypto など Node.js 標準モジュール、または Google Calendar API クライアントなど外部 npm パッケージ

許可されない依存の例:

❌ import { t } from '@/lib/i18n'
❌ import tailwindConfig from '../../../tailwind.config'
❌ import { useAstroRoute } from 'astro'

逆向き(corporate-site → bateson)の依存のみ許可する。bateson は corporate-site を「知らない」状態を維持する。この境界が core / extension の設計上の分離線だ。Phase C のリポジトリ抽出時に、この方向性が成立していれば git filter-branch でそのまま切り出せる。

規約 2: tenantId を public API シグネチャに最初から含める

Phase A では tenantId = 'yakumo' 固定で構わない。重要なのは シグネチャに tenantId が存在すること だ。

// ✅ Phase A でもシグネチャに tenantId を持つ
listAvailableSlots({ tenantId, eventTypeId, rangeStart, rangeEnd })
createBooking({ tenantId, eventTypeId, startAt, endAt, guest })
cancelBooking({ tenantId, cancelToken, reason? })

Phase B で multi-tenant 化するとき、シグネチャはそのまま使える。storage adapter の内部で tenant ごとのデータ領域を分けるだけで済む。

tenantId を省略した場合の Phase B コストを具体的に示す。public API の破壊的変更はすべての呼び出し元(UI / API endpoint / E2E テスト)の同時変更を要求する。型エラーが全ファイルに伝播し、修正の完了まで tsc --noEmit が通らない状態が続く。規約 2 はその状態を Phase A の段階で一行のシグネチャ設計により回避する。

規約 3: adapter interface による外部システム抽象化

bateson が依存する外部システムは 4 種類——カレンダー、メール、ストレージ、リマインダースケジューラ——だ。それぞれに interface を定義し、実装は adapter として差し替え可能にする。

bateson/adapters/
├── calendar/
│   ├── interface.ts   ← CalendarProvider 型定義
│   └── google.ts      ← Google Calendar 実装
├── email/
│   ├── interface.ts   ← EmailProvider 型定義
│   └── gmail.ts       ← Gmail API 実装
├── storage/
│   ├── interface.ts   ← BookingStorage 型定義
│   └── google-sheets.ts ← Google Sheets 実装
└── scheduler/
    ├── interface.ts   ← ReminderScheduler 型定義
    └── vercel-cron.ts ← Vercel Cron polling 実装

Phase A の Yakumo 実装では Google Calendar / Gmail / Sheets / Vercel Cron を使っているが、別テナントが Outlook / SendGrid / Postgres / AWS Lambda を使いたければ、adapter を 1 つ追加するだけで対応できる。core ロジックは変わらない。

この設計が正しく機能することは、リマインダースケジューラの実装方針が変わった経緯 で実証されている。設計初期は Upstash QStash を検討していたが、Vercel Cron + Sheets polling に切り替えた(外部依存ゼロ・同一スタック・1 分精度で実用十分という判断)。このとき core ロジックは一行も変わらず、scheduler/interface.ts の contract を満たす新しい adapter を書き換えるだけで済んだ。

規約 4: テンプレート DI(dependency injection)

createBookingCore は templates を引数として受け取る設計になっている。

createBookingCore({
  config,
  calendar,
  storage,
  email,
  scheduler,
  templates: {
    confirmation(input): EmailTemplateOutput { ... },
    cancellation(input): EmailTemplateOutput { ... },
    reschedule(input): EmailTemplateOutput { ... },
    reminder(input): EmailTemplateOutput { ... },
  },
  tokenSecret,
})

confirmation / cancellation / reschedule / reminder の各テンプレートは関数として注入する。bateson の core は「どのタイミングでテンプレートを呼ぶか」だけを知っている。「どんな文面を送るか」は知らない。

八雲のブランドテキスト・ロゴ・カラー・添付資料は src/lib/booking-yakumo/templates/ で注入する。将来、別テナント向けにブランド入れ替えが必要になったとき、templates の実装だけを差し替えれば済む。

4 規約を守ったコードと守らなかったコードの違いは、Phase A では目に見えない。Phase B に差し掛かったとき、守った側は数日でテナント分離が終わり、守らなかった側は全関数を書き直している。


命名 — なぜ bateson か

Gregory Bateson の ecosystem / context theory

bateson という名前は、人類学者・認識論者の Gregory Bateson(1904-1980)から取っている。彼の主著 “Steps to an Ecology of Mind”(1972)は、情報・文脈・生態系の関係を論じた思考の集積だ。

Bateson の中心的な問いは「文脈が意味を生む」という構造にある。単体の刺激(メッセージ)は文脈がなければ意味を持たない。文脈が変われば同じ刺激でも異なる意味を持つ。情報は孤立して存在せず、常に context の中にある。

「the difference that makes a difference」 — engine はシステム全体の文脈の中で違いを生む

Bateson の最も引用される言葉の一つが「a difference that makes a difference」(差を生む差)だ。情報とは「差異の差異」であり、文脈の中でのみ意味を持つ。

予約 SaaS が「単なる日程調整ツール」に終わる理由は、context から切り離されているからだ。bateson という命名は「engine が単独で意味を持つのではなく、システム全体のコンテキストの中で違いを生む」という設計意図を表している。面談予約は業務課題ヒアリングの前段であり、エージェント開発提案の入口であり、受注という結果への経路だ。孤立した日程調整ツールではなく、ecology の中の一要素として設計する——その意図を命名に折り込んでいる。

lib カテゴリの人名空間

八雲の src/lib/ には人名を冠したエンジンが並走している。mcluhan(Marshall McLuhan、“The medium is the message”)と bateson(Gregory Bateson、“the difference that makes a difference”)は対構造をなす。

Marshall McLuhan が「媒体それ自体がメッセージ」と論じたように、mcluhan engine は記事の内容より記事を運用する構造こそが owned media のメッセージを伝えると設計されている。Gregory Bateson が「文脈の中で意味が生まれる」と論じたように、bateson engine は予約という行為をフローの context の中に位置づける。

2 つの思想は「media / message / context / ecology」という共通の概念圏を持つ。命名が対構造になっているのは偶然ではなく、設計の意図を思想家の名前で表現した結果だ。


技術詳細: 4 規約の実装 {#tech-detail}

ここからは実際のコード断片を用いて、4 抽象化規約の実装を具体的に説明する。tech spoke(詳細記事)の準備が整っていない段階では、この H2 が最も詳細な技術ドキュメントになる。

adapter pattern の核 — 4 interface の定義

4 種類の adapter はそれぞれ TypeScript の interface として定義されている。実装の差し替えを保証するのはこの型 contract だ。

CalendarProvider(adapters/calendar/interface.ts

export interface CalendarProvider {
  freeBusy(input: FreeBusyInput): Promise<FreeBusyResult>;
  createEvent(input: CreateCalendarEventInput): Promise<CreateCalendarEventResult>;
  updateEvent(input: UpdateCalendarEventInput): Promise<void>;
  deleteEvent(input: DeleteCalendarEventInput): Promise<void>;
}

freeBusy は空き枠計算のために busy 時間を取得する。createEvent はカレンダーイベントを作成し、Google Meet URL を返す(createMeetUrl: true 時)。

EmailProvider(adapters/email/interface.ts

export interface EmailProvider {
  send(input: SendEmailInput): Promise<SendEmailResult>;
}

SendEmailInputtenantId / to / subject / html / text / attachments を持つ。Yakumo では Gmail API で実装されているが、SendGrid 実装に差し替えても core は変わらない。

BookingStorage(adapters/storage/interface.ts

export interface BookingStorage {
  create(input: CreateBookingStorageInput): Promise<{ id: string }>;
  findByTokenHash(tenantId: TenantId, tokenHash: string): Promise<Booking | null>;
  findById(tenantId: TenantId, id: string): Promise<Booking | null>;
  update(tenantId: TenantId, id: string, patch: Partial<...>): Promise<void>;
  logEvent(input: LogEventInput): Promise<void>;
  listDueReminders(input: ListDueRemindersInput): Promise<Booking[]>;
}

Yakumo では Google Sheets 実装を使っている。顧客データの SSOT が既存の Sheets であり、新たな外部依存を増やさないという判断からだ。Postgres / Notion への差し替えは adapter を 1 つ追加するだけで済む。

findByTokenHash はキャンセル / 変更フローの要だ。メールに渡したトークンの hash を照合して予約を特定する。hash 化して保存することで、Sheets に生のトークンが残らない設計になっている。

ReminderScheduler(adapters/scheduler/interface.ts

export interface ReminderScheduler {
  schedule(input: ScheduleInput): Promise<ScheduleResult>;
  cancel(input: CancelScheduleInput): Promise<void>;
}

Yakumo では Vercel Cron + Sheets polling で実装している。schedule はリマインダー配信時刻を Sheets の next_reminder_at カラムに記録し、Vercel Cron が 1 分ごとに polling して配信する方式だ。

createBookingCore の DI 構造 — 副作用は全て adapter に委譲

core の全機能は createBookingCore ファクトリ関数が返すオブジェクトに閉じている。ファクトリは deps として 4 adapter + config + templates + tokenSecret を受け取る。

export function createBookingCore(deps: {
  config: BookingConfig;
  calendar: CalendarProvider;
  storage: BookingStorage;
  email: EmailProvider;
  scheduler: ReminderScheduler;
  templates: {
    confirmation(input: TemplateInput): EmailTemplateOutput;
    cancellation(input: TemplateInput): EmailTemplateOutput;
    reschedule(input: TemplateInput): EmailTemplateOutput;
    reminder(input: TemplateInput): EmailTemplateOutput;
  };
  tokenSecret: string;
}): BookingCore {
  // ...
  return { listAvailableSlots, createBooking, cancelBooking, rescheduleBooking, processDueReminders };
}

BookingCore interface が返す 5 メソッドの中で発生する副作用(カレンダー操作 / メール送信 / ストレージ書き込み / スケジューラー登録)は、すべて依存注入された adapter 経由で実行される。

この構造の利点は 3 つある。

  1. テスト容易性: adapter をモックに差し替えればユニットテストが書ける。実際の Google Calendar / Gmail / Sheets を呼ばずに core ロジックを検証できる
  2. 環境差し替え: staging / production で異なる adapter を注入できる(例: staging では Sheets の別シートを使う)
  3. リポジトリ独立性: core が adapter の interface だけに依存しているので、Phase C のリポジトリ抽出後も adapter の実装だけ差し替えれば動く

tenantId 抽象化 — single-tenant 実装で multi-tenant 化を妨げない設計

core/types.tsTenantId = string として定義し、全 I/O 型に含める。

// core/types.ts
export type TenantId = string;

export type ListAvailableSlotsInput = {
  tenantId: TenantId;
  eventTypeId: string;
  rangeStart: string;   // ISO 8601
  rangeEnd: string;
};

export type CreateBookingInput = {
  tenantId: TenantId;
  eventTypeId: string;
  startAt: string;
  endAt: string;
  guest: Guest;
  metadata?: Record<string, unknown>;
};

export type CancelBookingInput = {
  tenantId: TenantId;
  cancelToken: string;
  reason?: string;
};

Phase A では tenantId: 'yakumo' が固定で渡される。Phase B では tenantId をキーに storage adapter 内のデータ領域を分岐させるだけで良い。Sheets 実装なら「tenantId + bookingId」を row のキーとして使うことで tenant 分離が完成する。

cancelToken / rescheduleToken の設計も tenantId 戦略と整合している。cancelToken === rescheduleToken(同一トークン)という設計判断は、Sheets 1 カラムに single hash で保存できる。token は HMAC-SHA256(bookingId:action, tokenSecret) で生成し、Sheets には SHA256(token) の hash のみ保存する。

UI も headless で出す — hooks と headless components

UI layer も bateson の 4 規約に従い、ロジックと見た目を分離している。

useAvailability hook

const { slots, isLoading, error, refetch } = useAvailability({
  tenantId: 'yakumo',
  eventTypeId: 'initial-meeting',
  rangeStart: '2026-06-01T00:00:00Z',
  rangeEnd:   '2026-06-02T00:00:00Z',
});

POST /api/booking/availability を fetch し、空き枠 AvailableSlot[] を返す。rangeStart / rangeEnd が変わると自動で refetch する。エラーは Error オブジェクトで返し、コンポーネント側でハンドリングする。

useBooking hook

const { isSubmitting, error, bookingId, meetUrl, submit } = useBooking({
  tenantId: 'yakumo',
  eventTypeId: 'initial-meeting',
});

await submit({ startAt, endAt, guest });

POST /api/booking/create を呼ぶ。成功後に bookingId / meetUrl を state に保存する。isSubmitting 中は二重 submit を防止する。

Calendar / TimeSlots / BookingFlow components

3 コンポーネントはすべて headless 寄りで、内蔵スタイルは最小限(cursor / display のみ)だ。Tailwind / CSS フレームワーク非依存で、classNames prop でスタイルを注入する設計になっている。

<Calendar
  value={selectedDate}
  onSelectDate={setSelectedDate}
  workingDays={[1, 2, 3, 4, 5]}
  advanceNoticeHours={24}
  maxAdvanceDays={30}
  timezone="Asia/Tokyo"
  locale="ja"
  classNames={{
    root: '...',
    daySelected: 'bg-accent text-white',
    dayDisabled: 'opacity-30',
  }}
/>

<table role="grid">aria-pressed によるアクセシビリティ対応、キーボードナビゲーション(←→↑↓ キー)も実装済みだ。

BookingFlow は Calendar + TimeSlots + Submit ボタンを統合した high-level component で、3 コンポーネントを個別に組む必要がない場合に使う。

<BookingFlow
  tenantId="yakumo"
  eventTypeId="initial-meeting"
  guest={{ name: '山田 太郎', email: 'yamada@example.com' }}
  onSuccess={({ bookingId, meetUrl }) => { /* 完了処理 */ }}
  onError={(err) => { /* エラー処理 */ }}
  workingDays={[1, 2, 3, 4, 5]}
  advanceNoticeHours={24}
  maxAdvanceDays={30}
  timezone="Asia/Tokyo"
  locale="ja"
/>

cancelToken / rescheduleToken の hash 設計と tenant 越境防止

cancelToken === rescheduleToken という設計は、Sheets の cancel_token_hash カラムを単一に保てる簡潔さを優先している。

// Token 生成: HMAC-SHA256
function generateToken(secret: string, bookingId: string, action: 'cancel' | 'reschedule'): string {
  const mac = createHmac('sha256', Buffer.from(secret, 'hex'));
  mac.update(`${bookingId}:${action}`);
  return base64url(mac.digest());
}

// Storage には hash のみ保存
function hashToken(token: string): string {
  return createHash('sha256').update(token).digest('hex');
}

tenant 越境防止は findByTokenHashtenantId を引数に取ることで担保されている。token hash と tenantId の両方が一致する行だけが返る。別テナントの token を知っていても、tenantId が異なれば予約を操作できない設計だ。

tech spoke 派生候補へのリンク

各規約の実装詳細は、準備でき次第 tech spoke として公開する予定だ。本稿は pillar として概念と構造を示し、深掘りは各 spoke に委ねる設計になっている。

準備中の spoke(公開時期未定):

  • adapter pattern の詳細 — Calendar / Email / Storage / Scheduler の実装解説(2026-06-bateson-adapter-pattern-detail
  • tenantId scoped storage — Sheets での multi-tenant 分離の実装(2026-06-bateson-tenantid-storage-scoping
  • headless UI の classNames 注入 — classNames prop 設計の詳細(2026-06-bateson-headless-ui-classnames
  • cancelToken 設計 — HMAC 生成・hash 保存・tenant 越境防止の詳細(2026-06-bateson-cancel-token-design

まとめ — Booking Engine の設計判断と Phase B-D 拡張余地

4 規約は設計責務の構造化

4 規約を再掲する。

  1. 逆向き依存のみ: src/lib/bateson/ は corporate-site のコードを一切 import しない
  2. tenantId 全引数化: Phase A から全 public API に tenantId を引数として持つ
  3. adapter で差し替え可能: Calendar / Email / Storage / Scheduler の 4 外部依存を interface 経由に閉じる
  4. テンプレ注入: メール文面・ブランド要素は createBookingCore に注入する関数として渡す

これらは実装上の不変性を保つ約束事だ。アーキテクチャを複雑にする必要はない。守るべき制約を明示して、それを Phase A から機械的に適用するだけで、Phase B-D への経路が確保される。

Phase B-D の次の実装課題

Phase B(multi-tenant リファクタ)が必要になったとき、4 規約を守っている状態なら以下だけで完了する。

  • storage adapter の内部実装: 全 read / write 処理に tenantId によるデータ領域フィルタを追加
  • BookingConfig の tenant 別分岐: 現在 Yakumo 固定の設定値を tenantId キーの Map に変更

シグネチャの変更は不要だ。呼び出し元(UI / API endpoint / E2E テスト)は Phase A のコードのまま動く。

Phase C(リポジトリ抽出)が必要になったとき、規約 1(逆向き依存のみ)が成立していれば git filter-branch の実行で src/lib/bateson/ を別リポジトリに切り出せる。corporate-site 側は import 先を npm package または git submodule に書き換えるだけで済む。

Phase D(プロダクト化)の配布形態——SaaS / OSS / クローズドライセンス——は Phase A-C では保留で構わない。dogfooding で肌感を掴んでから確定する。Phase A に必要なのは「Phase D の選択肢を閉ざさない設計判断」だけで、それはすでに 4 規約として実装されている。

Phase A の設計コストと Phase B-D の節約

4 規約を守るための追加コストは、Phase A では数時間から数日の設計規律に相当する。守らなかった実装と比べて、機能の変化はない。tenantId を引数に含める、外部依存を interface で抽象化する、テンプレートを注入可能にする——これらは機能追加ではなく構造の問題だ。

この構造的なコストが、Phase B-D に渡って数週間から数ヶ月の手戻りを防ぐ。bateson の設計に関する質問、または adapter / tenantId / DI 設計パターンについての相談は、本記事の末尾に置かれている問い合わせフォーム(/contact)から連絡してほしい。このフォームは bateson が実際に運用している sales engine そのものだ。読者の問い合わせが入った時点で、本記事で解説した adapter pattern が動いている。

SHARE X でシェア B! はてブ