payeverにいたのは二ヶ月だった。2020年の2月と3月 — パンデミックがすべてをロックダウンして誰もの計画が狂う、直前の二ヶ月だと気づいているかもしれない。短い在籍、具体的な仕事。これはその仕事の話だ。
payeverはECインフラを構築している — マーチャントが単一のプロダクトから複数チャネルの販売を管理できるプラットフォームだ。私が渡された具体的な問題:Mobile.de、Amazon.de、eBayとのインテグレーション用バックエンドマイクロサービス。三つのマーケットプレイス。三つの全く異なるデータモデル。プラットフォームが一貫して扱える統一ドメイン表現を一つ作る。
Mobile.deの車両リストAPIを直接触った経験を持つエンジニアに何人会ったことがある?そうだよね。
三つのマーケットプレイス、三つの異なる世界観
マーケットプレイス統合について最初に理解すべき重要なことは、各マーケットプレイスは単に異なるAPIではないということだ。それぞれコマースとは何かについて異なる概念モデルを持っている。
Amazonはカタログと在庫を別々に考える。商品にはASINがある — Amazonが「世界に存在するもの」に対して設定した正規識別子だ。あなたのリストは商品ではない — Amazonがすでに知っている商品を売るというあなたのオファーだ。この区別はアップデートのモデリング方法に深い影響を与える:価格を変更するとき、あなたはオファーを更新している。商品説明を変更するとき、Amazonが強いこだわりを持つAmazonの商品正規レコードを更新しようとしているかもしれない。フィードベースの更新フロー(当時はMWS、今はSP-API)は非同期かつ確認応答ベースだ。フィードを送信すると、サブミッションIDが返ってきて、結果をポーリングする。エラーハンドリングは単純なHTTPステータスコードではない。パースしなければならない構造化されたフィード結果ドキュメントだ。
eBayは直接オークション/リストモデルに近い。リストは完全に自分のもの — カタログの曖昧性解消レイヤーはない。これは単純に聞こえるし、ある意味そうだ。トレードオフは、リストは孤島であり、その完全なデータに責任を持つということだ。インテグレーションの複雑さはオーダーフローにシフトする:eBayのオーダーモデルには、プラットフォームの内部オーダーモデルにマッピングしなければならない、バイヤーメッセージ、フィードバック、紛争解決、返品開始という独自の概念がある。
Mobile.deはディナーパーティーで説明するのが面白い。ドイツ外ではほぼ誰も知らず、ソフトウェアの世界で直接触ったことがある人はほぼいないからだ。ドイツ最大の中古車マーケットプレイスだ。在庫モデルは高度に垂直 — 車両リストはメーカー、モデル、年式、グレード、走行距離、VIN、状態の特定分類タクソノミー、そしてドイツ市場で重要な検索可能属性(燃料タイプ、トランスミッション、排出ガスクラス)を持っている。汎用ECプラットフォームはこれをネイティブに理解しない。APIは十分に文書化されているが、横断的なリテールインテグレーションだけを経験した人には概念モデルが異質だ。
統一ドメインモデル問題
プラットフォームの仕事は、マーチャントにこれらすべてを管理する単一のインターフェースを提供することだ。つまりバックエンドの仕事は、三つすべてのプラットフォームでリストを忠実に表現し、更新をラウンドトリップできる単一の内部ドメインモデルを持つことだ。
ここで本当に難しくなる。内部モデルの「商品」は以下のための十分な情報を持たなければならない:
- 既知のASINに対するフィードエントリとしてAmazonに送信する、またはASINが存在しない場合は新しい商品作成リクエストをトリガーする
- カテゴリ、アイテム詳細、配送ポリシーを含むリストデータの完全な所有権でeBayに出品する
- AmazonもeBayも全く必要としない正しい車両タクソノミーフィールドでMobile.deに出品する
ナイーブなアプローチは、すべてのマーケットプレイスのすべてのフィールドを持つ巨大な商品スキーマを作ることだ。これは間違いだ。フィールドは単に異なるだけでなく、意味的に異なる。eBayの「Condition」(New、Used、For parts or not working等)はMobile.deの車両状態と同じ概念ではない。Mobile.deは独自の評価語彙を持っている。これらの差異をフラット化しようとする統一モデルは嘘をつくスキーマを生み出す。
正しいアプローチは、真にユニバーサルなフィールド(タイトル、価格、在庫数、メディア)を持つコアドメインモデルを用意し、マーケットプレイス固有の拡張を別途型付けしてコアレコードと並べて保存することだ。マーケットプレイスに公開するとき、コアレコードと適切な拡張を合成する。マーケットプレイスからアップデートを受け取るとき、それを分解して戻す。MongoDBのドキュメントモデルはこれに適している。拡張フィールドがマーケットプレイスによって異なり、リレーショナルテーブルでは機能しない固定スキーマを持たないからだ。
縫い目としてのNestJS
サービスレイヤーはNestJSだった — Angularにインスパイアされたモジュール構成を持つNode.js上のTypeScriptだ。このようなインテグレーション作業では、モジュールの境界が本当に役立つ:AmazonモジュールはAmazonのワイヤフォーマットを所有し、eBayモジュールはeBayのワイヤフォーマットを所有し、コア商品モジュールはドメインモデルを所有する。依存の方向は明確だ。Amazonモジュールの何もeBayのリスト表現に依存しない。
RabbitMQが非同期処理を担当した:処理に時間がかかるマーケットプレイスフィード、各マーケットプレイスからのインバウンドオーダー通知、マーチャントが価格を変更したときにすべてのアクティブなチャネルにファンアウトする必要がある在庫アップデート。ここのエクスチェンジルーティングはシンプルだった — インバウンドイベント用にマーケットプレイスごとに一つのエクスチェンジ、有効なすべてのチャネルパブリッシャーにファンアウトする内部商品アップデート用に一つのエクスチェンジ。パブリッシャーは内部アップデートをマーケットプレイス固有のワイヤフォーマットに変換する方法を知っているモジュールだ。
8週間と、その価値
二ヶ月は多くの時間ではない。インテグレーションが完成していたとは言わない。その期間に出荷したのは、三つのマーケットプレイスすべてにおけるコア商品プッシュパスとインバウンドオーダー同期だった。返品フロー、メッセージングインテグレーション、完全なエラー照合ループ — それはロードマップだった。
二ヶ月で十分なのは、ドメインを深く学んで有用な意見を持つことだ。私はMobile.deの車両タクソノミーが内側からどう見えるかを知っている。フィードがバリデーションに失敗したときのAmazonの非同期フィード確認フローがどう見えるかを知っている。eBayのカテゴリ固有のアイテム詳細要件がリストのライブ化を左右する方法を知っている。これらは抽象的なアーキテクチャの概念ではない — それはログを読んでいたときに実際に起きていた具体的な失敗モードだ。
これは、ほとんどのエンジニアが二ヶ月以上かけて意見を形成するインテグレーション考古学の一種だ。これら三つのシステムすべてに同時に取り組むエンジニアはほとんどいないからだ。圧縮されたタイムラインは、何が重要で何が偶発的な複雑さかについての明確さを強制した。
Fulcrumにインテグレーション規律を組み込んできた:ドメイン間の型付きコントラクト、安定したイベントスキーマ、外部システム向けのコンポーザブルなアダプタについての同じ考え方は、エージェントオーケストレーションに直接適用できる。ドメインが違っても問題は韻を踏んでいる。そしてpayeverのエンゲージメントと違って、Fulcrum上のタイムラインは二ヶ月ではない。