Tabelog Tech Blog

食べログの開発者による技術ブログです

他社システム連携にDDDのAnti-Corruption Layerを適用した話 ― 境界設計から整合性保証まで

はじめに

こんにちは。食べログカンパニー 開発本部 国内メディア開発部の城戸と申します。主に、他社とのシステム連携や広告系の業務を中心に担当しています。

本記事では、他社システムとの予約連携案件を通じて経験した、Anti-Corruption Layer(ACL)の設計・実装について紹介します。

外部予約連携における新たな取り組み

食べログでは、これまで他社システム経由でも予約を受け付ける外部予約連携に取り組んできました。従来の連携では、食べログの予約システムが提供する汎用的なAPIを他社システム側が呼び出す形で実現していましたが、今回、他社システムの仕様に合わせて食べログ側が個別対応のAPIを作成する必要がありました。

ここで懸念されたのが、保守コストの問題です。各社への個別実装を予約システムに組み込み続けると、連携先が増えるほど保守コストも線形に増大することが予想されました。

設計時に想定した3つの問題

予約システムに他社システムの仕様を直接組み込む実装を避けたかった理由は、以下の3つの問題を懸念したためです。

1. 外部仕様の変更が内部システムに伝播する

他社システムの仕様が変更されるたびに、予約システムの修正が必要になります。本来、外部連携の仕様変更は予約システムのコアロジックに影響を与えるべきではありませんが、直接組み込む実装では、この影響を遮断できません。

2. 予約チームの負担が増大する

連携先が増えるたびに、予約チームへの変更要求が増えていきます。予約システムは食べログの中核機能であり、外部連携対応によって予約チームが本来の予約プロダクト開発に注力しにくくなることが懸念されました。

3. 新規連携追加のコストが増大する

複数の他社システムの仕様が予約システム内に混在すると、「この処理はA社専用」「この分岐はB社のみ」といった条件分岐が増えていきます。結果として、新たな連携先を追加する際に既存コードへの影響を考慮する必要があり、拡張コストが増大します。

これらの問題を解決するため、他社システムの仕様を予約システムから切り離し、変更の影響を局所化できる設計が必要でした。

私が担った役割

この課題に対し、チームではドメイン駆動設計におけるAnti-Corruption Layer(ACL)パターンを採用する方針を決めました。私はその具体的な設計・実装を主導する役割を担いました。

特に以下の点について、技術的な判断を要しました。

  • 境界の定義: 何をACLで吸収し、何を内部システムに伝播させるか
  • 整合性の担保: 情報提供時と予約時のタイムラグをどう扱うか
  • 運用時の課題対応: タイムアウトなどによる状態不整合にどう対処するか

設計時には、ACLを境界として明確に定義することで、以下を実現できると考えていました。

  • 他社システムごとの違いをACLで吸収し、予約システムへの影響を最小化する
  • 外部連携チームと予約チームの責務を分離し、チーム間の依存関係を減らす
  • 2社目以降の連携時に、同じ設計パターンを再利用できる拡張性を確保する

以降、この設計思想と具体的な実装について、詳しく紹介していきます。

ACLの設計思想

Anti-Corruption Layerとは

本記事で採用したのは、ドメイン駆動設計(DDD)におけるAnti-Corruption Layerパターンです。以降、ACLと表記します。

ACLは、外部システムと内部システムの間に立つ中間層として機能します。その役割は、外部システムごとに異なる仕様を吸収し、内部システムには統一されたインターフェースで連携することです。

例えば、A社のシステムとB社のシステムで全く異なるAPI仕様があったとしても、ACLがそれぞれの違いを吸収することで、内部の予約システムは「どの他社システムと連携しているか」を意識する必要がなくなります。

最も重要な判断: 境界をどこに引くか

ACL設計で最も重要なのは、「何をACLで吸収し、何を内部システムに伝播させるか」という境界の定義です。この境界が曖昧だと、結局は内部システムが外部仕様の影響を直接受けてしまい、ACLを導入した意味が薄れてしまいます。

本案件では、以下のように境界を定義しました。

ACL内で吸収するもの

他社システム固有の仕様を全てACL内で処理します。

  • データ形式の変換: 他社システムのAPI仕様に合わせたリクエスト・レスポンスの形式
  • 他社システム固有のビジネスルール: 特定の他社システムでのみ必要な処理や判定

内部予約システムに残すもの

予約処理のビジネスロジックは予約システムに残します。

  • 予約の可否判定
  • 予約枠の在庫管理
  • コアな予約処理

この線引きにより、予約システムは「食べログとしての予約処理」に専念できます。

なぜこの境界にしたのか

境界を定義する際は、チーム間の責務分離を意識しました。

食べログでは、予約システムを担当する予約チームと、外部連携を担当する外部連携チームが存在します。もし予約システムに他社連携の仕様が入り込んだ場合、以下のような問題が発生します。

  • 予約チームは、外部連携の仕様変更のたびに対応が必要になる
  • 外部連携チームが新規連携を追加する際、予約チームへの依頼が必須になる
  • 両チームの依存関係が強くなり、開発スピードが低下する

境界を明確にすることで、以下を実現できます。

  • 予約チームは予約処理のコアロジックに集中し、他社システムの仕様変更の影響を受けない
  • 外部連携チームはACLの実装・保守に責任を持ち、予約チームへの依頼を最小化できる

技術的な境界定義が、チーム間の協働をスムーズにすることにもつながっていました。

チームとシステムの責務分離

ACLに持たせた3つの責務

境界を定義した上で、本案件ではACLに以下の3つの責務を持たせました。

1. データ形式の変換

他社システムと内部予約システム間のデータ形式を相互に変換します。これはACLの最も基本的な責務です。

2. 整合性チェック

外部連携では、情報提供のタイミングと実際の予約のタイミングにズレが生じます。このタイムラグによる情報の不整合を検知する仕組みが必要でした。

3. 状態不整合への対処

タイムアウトなどで予約状態にズレが生じた場合、どう対処するかの判断も必要でした。

以降、これらの責務ごとに、具体的な実装と判断プロセスを紹介していきます。

ACLの実装

データ変換 - ACLの基本責務

まず、ACLの最も基本的な責務であるデータ変換について説明します。

システム構成とデータの流れ

ACLは、他社システムと内部予約システムの間に配置されます。

ACLの配置とデータの流れ

予約リクエストが来た場合、データは以下のように流れます。

  1. 他社システム → ACL: 他社システム固有のデータ形式でデータを受け取る
  2. ACL内での変換: 内部予約システムが理解できるデータ形式に変換
  3. ACL → 内部予約システム: 変換したデータで内部予約システムの既存の予約APIを呼び出す
  4. 内部予約システム → ACL: 予約APIからレスポンスを受け取る
  5. ACL内での変換: 他社システムの仕様に合わせてデータ形式を変換
  6. ACL → 他社システム: 変換したレスポンスを返却

データ変換はACLの基本的な責務ですが、本案件ではそれだけでは不十分でした。次に、その応用的な責務について説明します。

整合性チェック - 情報のタイムラグへの対処

データ変換に加えて、もう1つ重要な課題がありました。情報提供のタイミングと予約のタイミングのズレです。

外部連携特有の課題: 情報のタイムラグ

他社システムとの連携では、予約が以下のような流れで行われます。

  1. 情報提供: 食べログから他社システムへ、事前に予約可能なコース情報(コース名、料金、内容など)を送信
  2. 保持・表示: 他社システムは受け取った情報を自システムのDBに保存し、ユーザーに表示
  3. 予約リクエスト: ユーザーが他社システム上でコースを選択し、予約リクエストが食べログに送られる

ここで問題になるのが、情報提供から予約操作までのタイムラグです。たとえ情報をリアルタイムに送信したとしても、ユーザーによる内容確認から予約するまでの時間は避けられず、その間に例えば以下のようなコース情報の変更が起こりえます。

  • 店舗がコース内容を変更する
  • 料金を更新する
  • コース提供を一時停止する

他社システムは古いコース情報を保持したまま予約リクエストを送ってくるため、ユーザーの閲覧情報と実際の予約内容に乖離が生じます。

この問題を防ぐため、予約リクエスト時に「情報提供時点から内容が変わっていないか」をチェックする仕組みが必要でした。

最初のアイデア: 既存の差分検知の流用

最初に考えたのは、バージョン番号やupdated_atなど、既存の差分検知の仕組みを整合性チェックに流用することでした。既存の仕組みを使えるため、簡単に実装できると考えました。

設計中の気づき: 過剰検知のリスク

整合性チェックのフローを図に書き起こしている際に、「バージョン管理の仕組みを流用すると、整合性チェックに無関係なカラムの変更まで検知してしまうのでは?」という疑問が浮かびました。

対象テーブルの構成を改めて確認したところ、外部連携で整合性チェックが必要な項目以外にも、多くのカラムが存在していました。

この仕組みの問題点は以下の通りです。

  • テーブル単位のバージョン管理では、テーブル全体の変更を検知する
  • つまり、外部連携の整合性チェックに不要なカラムが更新されても、変更として検知してしまう
  • 結果として、「実際にはコース情報が変わっていないのにエラーになる」という誤検知の頻発が懸念される

さらに、将来的に差分検知の対象項目を追加・変更したい場合、バージョン管理の仕組み自体を見直す必要が出てきます。既存システムへの影響を考えると、現実的ではありませんでした。

方針転換: ハッシュ値方式の採用

そこで、外部連携に必要なカラムだけを選択してハッシュ計算する方式に変更しました。

この方式の利点は以下の通りです。

  • 必要な項目だけを対象にできるため、誤検知がない
  • 将来的に対象項目を追加・変更する際も、ハッシュ計算ロジックを修正するだけで済む

ハッシュ値による整合性チェックのフロー

この実装により、他社連携に関係する項目のみを対象とした、精度の高い整合性チェックを実現できました。

状態不整合への対処

整合性チェックで情報のズレは検知できるようになりましたが、もう1つ懸念される問題がありました。タイムアウトなどによる状態不整合です。

起こりうる問題のシナリオ

例えば、予約処理の実行中に他社システム側でタイムアウトが発生すると、以下のような状況になります。

  • 食べログ側: 予約処理は正常に完了し、DBには予約データが保存されている
  • 他社システム側: タイムアウトでレスポンスを受け取れず、予約が完了したか分からない

状態不整合のシナリオ

他社システム側でリトライすれば解決できる場合もありますが、回数には上限があり、それでも解決しないケースに備える必要がありました。

この問題に対し、どう対処するか。設計段階で3つのアプローチを検討しました。

検討した3つのアプローチ

アプローチ1: 店舗への「予約キャンセル操作」依頼

不整合が発生したら、食べログ側から店舗に連絡し、予約をキャンセルしてもらう運用です。

メリット: 誤キャンセルを防げる。

デメリット: 人手を介するためスケールしない。

アプローチ2: システム上での予約データ同期

両社のシステム間で予約データを同期し、自動的に不整合を解消する仕組みです。

メリット: 自動で解消され、個別対応が不要。

デメリット: 開発期間が長い。

アプローチ3: ユーザーへの通知・エラー表示による解決促進

不整合が発生した場合、ユーザーに適切なエラーや通知を表示し、認識漏れを防ぐ対応です。

メリット: 開発・保守コストが最小限。

デメリット: 不整合自体は解消されない。

採用した判断: アプローチ3

結論として、本案件ではアプローチ3を採用しました。

判断の評価軸

  1. 不整合の発生頻度: 他社システムの仕様と実績から推定される発生頻度
  2. ビジネスへの影響: 万が一不整合が発生した場合の影響の大きさ
  3. 実現可能性: 初回リリースのスケジュール内で実装できるか

なぜアプローチ1は選ばなかったのか

人手を介する運用は、以下の理由でスケールしないと判断しました。

  • 店舗側は不整合のたびにキャンセル操作が必要になり、負担が大きい
  • 社内でも店舗への連絡・調整業務が都度発生し、予約件数の増加に対応できない
  • 連絡から完了までのタイムラグの間、ユーザーと店舗の認識のズレが続く

なぜアプローチ2は(今回は)選ばなかったのか

予約データの同期は技術的に理想的ですが、以下の理由で初回リリースでは見送りました。

  • 双方のシステム間で予約状態を同期する仕組みの設計・実装には、両社での調整を含めて相応の期間が必要
  • 初回リリースのスケジュールでは現実的ではない
  • まずはアプローチ3で開始し、運用状況を見て段階的に同期機能を追加する方針が合理的

なぜアプローチ3を選んだのか

以下の3点から、アプローチ3が現実的と判断しました。

1. 発生頻度が許容範囲内

  • 他社システムの仕様と過去の実績から、不整合の発生は低い頻度と推定
  • 大半はリトライで解決可能

2. ビジネスへの影響が限定的

  • ユーザーには他社システム上で適切な通知やエラーが表示される
  • 店舗は店舗向けの予約管理画面で予約状況を確認でき、ユーザーからの問い合わせに対応できる
  • 致命的な問題にはならない

3. 初回リリースに間に合い、将来の改善余地を残せる

  • 開発期間が短く、スケジュール的に現実的
  • 運用開始後の実際の発生状況を見て、アプローチ2への移行を検討できる

運用中の対応: ACL設計が活きた事例

ここまで、設計段階での判断を紹介してきましたが、実際に運用が始まると、新たな改善要求が出てくることがあります。

運用開始後に発生した、エラーコード詳細化の要求と、その対応判断を紹介します。

運用開始後に発生した課題

他社システムとの連携開始後、以下のような要望がありました。

キャンセル処理が失敗した際、失敗理由に応じた詳細なエラーコードが欲しい、というものです。

例えば以下のようなものです。

  • 「既にキャンセル済みの予約です」
  • 「キャンセル期限を過ぎています」
  • 「予約が見つかりません」

これらを区別できるエラーコードがあれば、ユーザーに適切なメッセージを表示できます。

判断のジレンマ: 理想と現実

この要求に対し、技術的には2つの選択肢がありました。

選択肢1: 予約システム側でエラー判定を詳細化する

キャンセル処理を行う予約システム側で、失敗理由を詳細に判定してエラーコードを返すようにする。これが最もシンプルで、理想的なアプローチです。

選択肢2: ACL側でエラー判定を行う

ACLがキャンセルAPIの失敗を検知した後、追加で予約情報を取得して状態を確認し、適切なエラーコードを判断する。ACL内での処理が増えますが、予約システムには手を入れない方法です。

選択肢1が理想的ですが、予約システムのキャンセル処理は食べログのWebサイトやアプリなど既存の多くの箇所で利用されています。変更には影響範囲の調査、テスト、リリース調整など、相応のコストと時間がかかります。

採用した判断: ACLで吸収する

結論として、選択肢2(ACL側でエラー判定)を採用しました。

キャンセルAPIが失敗した場合、ACLが予約情報取得APIを追加で呼び出し、取得した予約の現在状態に基づいて適切なエラーコードを判断・返却する実装としました。

修正箇所はACL内の2ファイル(実装約30行、テスト約90行)に留まり、予約システムは無修正で済みました。

まとめ

ACL設計で最も重要なこと

本記事では、外部予約連携におけるACLの設計と実装を紹介してきました。振り返ると、最も重要だったのは「何を境界とするか」の定義でした。

私は「他社システムとのインターフェース」をACLの責務と定義し、予約処理のビジネスロジックは予約システムに残しました。この境界定義を設計初期に明確化したことで、実装中の判断や運用時の対応を一貫性を持って進められました。

同じ課題に取り組む方へのアドバイス

境界の定義に時間をかける

ACL導入時は、まず境界の定義に十分な時間をかけることをおすすめします。

「外部システムの仕様変更による影響を局所化する」という目的を常に意識し、何をACLで吸収し、何を内部に伝播させるかを明確にすることが重要です。

この定義が曖昧だと、結局は内部システムが外部の影響を受けてしまい、ACLを導入した意味が薄れてしまいます。

理想と現実のバランスを取る

技術的に理想的な設計であっても、スケジュール、既存システムへの影響、双方のシステム制約を考慮し、現実的な着地点を見つけることが実務では重要です。

特に、他社システムとの連携では、自社だけでなく相手システムの制約も考慮する必要があります。協議・調整は、技術的な実装と同じくらい重要なプロセスです。

おわりに

最後までお読みいただき、ありがとうございました。

昨今、AIツールがコード生成や設計の提案を一瞬で行ってくれる時代になりましたが、生成されたコードや提案された設計を評価・選択し、最終的に責任を持つのは現時点ではエンジニアの役割です。

設計パターンの引き出しを持つことで、AIツールが生成した選択肢を適切に評価し、より効果的に活用できるはずです。

本記事が、他社システムとの連携設計に取り組む方の参考になれば幸いです。


最後に、食べログでは一緒に働く仲間を募集しています。
ご興味を持っていただけた方は是非下記の採用情報ページをチェックしてみて下さい!