
設計
イベント
マガジン
技術ブログ
p.fontbold{ font-weight: bold; } p.codeboxbefore{ margin-bottom:-70px; } @media screen and (max-width: 575px) { p.codeboxbefore{ margin-bottom:-13.4vw; } } この記事は Merpay & Mercoin Tech Openness Month 2026 の 13 日目の記事です。 こんにちは。Growth Platform Team でメルペイのポイント還元キャンペーン基盤である Santa サービスの開発を担当している @hasegway です。 なお、タイトルに登場する「Otoku Revolution」とは、コード決済を一定回数使うたびに必ず値引き体験が届く新企画のキャンペーン (本記事では「コード決済の回数連動キャンペーン」と呼びます) の社内呼称です。詳しくは本連載 17 日目の @yutaro の記事を楽しみにしていてください。本記事では、長く運用してきた Santa サービスをルールベースの汎用基盤 (以降「Rulebase 基盤」と呼びます) として書き直したリファクタリングの話と、新基盤の最初のキャンペーンとして「コード決済の回数連動キャンペーン」を立ち上げた話を取り上げます。順を追ってお話しする前に、まず Santa という基幹サービスがどのような歴史を経て、どんな負債を抱えるに至ったかについてお話しさせてください。 Santa の歴史 Santa という名前は、初期に 「使った翌日にバッチ処理でポイントを付与する」 サービスだったところから来ています。夜のうちに溜まったイベントを翌朝まとめて配って回る、シンプルな仕組みでした (初期の仕組みについては メルペイのキャンペーンを支えるサンタの秘密 が詳しいです)。 それから何年も経て、今では 1 日数百万イベントを処理し、メルカリ / メルペイのさまざまな利用シーンでポイント付与を行う基幹サービスへと成長しました。この成長の過程で一貫していたのは、 「キャンペーンの種別ごとに専用のイベントパイプラインを実装する」 スタイルで機能を積み重ねてきたことです。それぞれ独自のテーブル、独自のイベントハンドラ、独自の Cap (上限) ロジック、独自のポイント付与フローを持っていました。 2021 年のフィルタリング機能 は「複数の条件を AND/OR で組み合わせる」 発想を Santa に持ち込んだ、汎用化の最初の一歩でした。 2022 年のメルカード常時還元 は、その上に「メルカードステージ別の還元率」「複数月にまたがる Provision (引当金) 管理」 といった精緻なロジックを乗せた大規模なパイプラインで、現在でも Santa 最大のパイプラインです。それでも、キャンペーン本体のコードは CampaignType ごとに別物のままでした。 この構造は当初の要件においては合理的でした。しかしキャンペーン要件の複雑化とともに、コードの再利用性が低い設計による開発速度の低下、バグのキャンペーン別対応による運用負荷の増大といった課題が積み重なっていきました。2025年夏の時点では、Santa エンジニアチーム内でこれら課題への共通理解ができており、次世代キャンペーン構造のラフ設計まで進んでいました。ただし当時はスケジュール上の制約で本格着手を見送り、2025年10月にローンチしたメルカリモバイル向け特典キャンペーンは既存システムを拡張する形で実装しました。 その後、PoCを進め、2025年12月にチーム向けに成果を発表。2026年春にかけて、汎用キャンペーンテーブルにルール評価とアクション実行モジュールを組み合わせた Rulebase 基盤を新規に構築しました。現時点では「コード決済の回数連動キャンペーン」を1件稼働させている段階で、既存のキャンペーン群は引き続き従来の専用パイプラインで動いています。これら既存キャンペーンの段階的な移行は、これから先のフェーズです。 専用パイプラインが抱えた負債 Rulebase 基盤を作る前の Santa は、キャンペーンの種別(CampaignType) ごとに「専用パイプライン」を実装する、というスタイルで成長してきました。代表的なキャンペーン種別と、各種別の代表的なキャンペーン企画は次のとおりです。 キャンペーン種別 主なキャンペーン企画 購入時還元 「買ってお得!dポイント」など メルペイの定額払いの還元 「はじめての定額払いキャンペーン」など メルペイスマートマネーの還元 「初回利用」「カムバックキャンペーン」など メルカード還元 「メルカード常時還元」など これらの種別はそれぞれが、専用のテーブル群、決済や返済を受け取る Pub/Sub の入り口、ビジネスロジックを担う Interactor、ポイント付与履歴テーブルへの書き込みパス、付与上限の計算ロジックを抱えています。また、企画ごとの細かい要件の実現のため、基本のパイプラインの中にさまざまな分岐処理が加えられています。新たにキャンペーン企画を 1 本立てるたびに、これらのさまざまなレイヤーを個別に調査・変更しなければならない、というのが Santa の標準作業でした。 そして、長年運用するなかでこの構造がいくつかの負債を生んでいました。 累積した運用負債 まず、専用パイプラインを実装するスタイルでは、新たなキャンペーン要件のための追加開発が横展開して再利用しづらい問題がありました。また、当初に想定していたキャンペーン要件では考えられなかった新たな要件は、個別実装で対応せざるを得ないこともありました。これらは徐々に開発速度の低下やリグレッションテストの複雑化を招いていきました。特に MercardCampaignType ではこの問題が顕著で、リファクタリングに踏み切る直接的な引き金になりました。 MercardCampaignType が「なんでも置き場」化していった 2022 年にローンチした MercardCampaign パイプラインは、当初「利用ステージ別の還元率」と「清算起点のリアルタイムのポイント付与」を素直に扱うことを想定した、シンプルな常時還元キャンペーン向けの設計でした。 その後、メルカードまわりのキャンペーン要件は急速に広がっていきます。累計購入額連動キャンペーン、メルカリ NFT 決済への対応、メルカード ゴールド、メルカリモバイル契約者向けの特典など、どれも「メルカード保有者・利用者」という共通の文脈はあるものの、設計当時の想定にはなかった要件ばかりです。それでも置き場としては MercardCampaignType が最も適切だったため、これらの新要件は順次 MercardCampaignType の上に実装されていきます。本来シンプルに設計されていた入れ物に想定外の機能が次々と追加され、還元率算出や会計コード指定などの仕組みが本来の用途を超えて流用されるようになっていきました。 ここでは、特に歪みが目に見える形で表面化した3 つの事例 — 累計購入型 (2024 年 9 月)、メルカリ NFT 会計コード差し替え対応 (2025 年 12 月)、メルカリモバイル向け特典キャンペーン (2025 年 10 月) — を順に見ていきます。 累計購入型 と メルカリ NFT 会計コード差し替え 累計購入額連動キャンペーンでは、購入金額が複数のしきい値を順に超えるたびに段階的にポイントが付与されます (例: 累計購入額に応じて最大 P1,500 もらえる)。このキャンペーンが MercardCampaignType に投入され、本来の還元率スキーマに「購入累積トラッカー」型の挙動が乗りました。メルカリ NFT 会計コード差し替え対応では「キャンペーンの各種条件は他と共通にしつつNFT取引のみ会計コードを差し替える」という要件のため、コード内に分岐処理が追加され、キャンペーン設定値が複雑化しました。どちらもメルカード保有者向けの施策ではあるものの、当初想定の責務範囲を超えた要件です。 そして3 例目の、もっとも歪みが大きく出たケースが、メルカリモバイル向け特典キャンペーンです。 メルカリモバイル向け特典キャンペーン (2025 年 10 月) メルカリモバイル向け特典キャンペーンの要件は、 3 種類のメルカードステータス (保有無し / 通常版 / ゴールド) × 4 種類のモバイルデータプラン (4GB / 10GB / 20GB / 40GB) = 12 の独立したキャンペーンパターンが毎月必要、というものでした。 実装上は、本来メルカードステージ別の還元レートを保持するDBフィールドが「データプラン別のレート」の入れ物として流用されました。3 ステータス ×4 データプランの 12 組み合わせを、メルカード用テーブルの行として毎月生成する運用です。 また、お客さまのメルカードステータスやモバイルデータプランは日々変わりうるため変化に合わせて還元レートや月々の上限を計算し直す必要があります。そして、同じお客さまが同月内で別の組み合わせ向けキャンペーンにも二重にマッチして重複付与が起こりうる、というリスクも残ります。後者の重複付与を防ぐために、コード側にはこんな雰囲気のハードコードマップが入りました (簡略化したイメージ)。 // ポイント重複付与防止のため、キャンペーンIDをコードに埋め込み TemporaryCampaignIDMapping = map[string]string{ "202510": "campaign-id-1", "202511": "campaign-id-2", // 毎月追加が必要... そして各所に、データプラン別ステージを判定する if 文が散らばりました。 こうして、毎月 12 パターン分のデータ追加運用が必要な「Temporary」ハードコードマップが本番に居続け(Temporaryとは・・)、モバイル専用ステージを分岐させる if 文がポイント計算・フィルタ評価・API レスポンスに散在し、将来データプランが1つ増えるたびにコード変更とデプロイが必要になり、MercardCampaignType 全体のリグレッションテストも巻き込む、という構造が出来上がっていきます。当初は 「モバイルキャンペーンをアーキテクチャ刷新のきっかけにして抜本対応する」 計画もありましたが、ローンチ期日との両立が難しく、最終的に既存パイプラインを拡張する判断を取りました。当時の制約下では合理的な選択ですが、根本的な構造課題は持ち越し、運用負荷は増えています。 3 事例ともビジネス文脈では筋が通っている一方、「メルカード還元の入れ物」が「メルカード周辺キャンペーン全般の入れ物」として使われ、MercardCampaignType のスキーマと責務範囲が押し広げられてきたのが当時の状態で、そのひずみは無視できない大きさになっていました。 Rulebase 基盤の設計 このリファクタリングそのものは前から検討していたものの、ローンチ責任との両立が難しく、本格着手は半年寝かせています。その間も内部で PoC は進め、本実装で固めた方針は「キャンペーンの挙動を、専用コードから設定データへ」というシンプルなものです。 より具体的には、 「どのようなきっかけで動くか」 (TriggerType)、 「どのような条件でマッチさせるか」 (RuleCondition と、その評価を担う RuleEvaluator)、 「何をするか」 (ActionExecutor) という3 つの軸を、できるだけ atomic な (再利用可能なサイズの) 部品として定義し、その組み合わせで多様なキャンペーン要件に対応する、というのが基本的な発想です。従来のように「メルカードキャンペーン専用のロジックを書き下ろす」のではなく、Trigger / Condition / Action を小さな部品として用意し、キャンペーン定義はその組み合わせとして書く、という発想です。専用パイプライン時代との根本的な違いはここにあります。一度実装した Condition や Action を別の CampaignType から設定値だけで呼び出せたり、動的な条件分岐や繰り返し条件を1 つのキャンペーン定義の中で表現できたりする能力も、ここから生まれます。 全体像 チーム内発表でも、次の対比を使って説明しました。 As-Is (メルカード専用ハンドラの内部に if が積まれている) func (h *MercardCampaignHandler) Execute(event Event) { if user.Stage == StageA { if user.Status == "Active" { if !h.HasReward(user.ID, event.ID) { points = amount * 0.03 h.RewriteRewardHistory(user.ID, event.ID) } } } else if user.Stage == StageB { if user.Status == "Gold" { points = amount * 0.10 if h.HasReward(user.ID, event.ID) { h.RewriteReward(user.ID, event.ID, points) } } } } To-Be (キャンペーン定義は JSON データ、評価エンジンは汎用) { "rule_id": "mobile-std-4gb", "conditions": [ {"type": "user_attribute", "params": {"stage": "Standard", "plan": "4GB"}}, {"type": "period", "params": {"start": "2025-10-01", "end": "2025-10-31"}}, {"type": "payment_attribute", "params": {"transaction_type": "code_payment"}} ], "action": { "type": "ADD_POINTS", "params": {"rate": 0.05, "currency_points": 60, "monthly_cap": 200} } } この JSON を Spanner に永続化したスキーマが次の3 テーブルです。Campaigns 配下に CampaignRules、その配下に RuleConditions を INTERLEAVE IN PARENT で並べる構造になっています。 -- 大枠の宣言: 期間・CampaignType・キャンペーン固有設定 (JSON) Campaigns( CampaignID, CampaignType, StartAt, EndAt, Metadata, ... ) -- 1 キャンペーン内の複数ルール: 何をトリガに、どう集計し、どんなアクションを取るか CampaignRules( CampaignID, RuleID, TriggerType, CalculationType, ActionType, ActionParams (JSON), Priority, Enabled ) INTERLEAVE IN PARENT Campaigns -- 1 ルール内の複数条件: AND/OR グループで合成 RuleConditions( CampaignID, RuleID, ConditionID, ConditionType, ConditionParams (JSON), ConditionGroup ) INTERLEAVE IN PARENT CampaignRules これに対応する評価エンジン (Rule Evaluation Engine) を新設し、以下のような4 層構造の EvaluationData を入力として走らせます。 Layer 内容 Event Data 受信した Pub/Sub イベント Rule & Campaign DB から読んだ CampaignRule + RuleConditions User Data お客さまの属性 (カード種類、利用履歴など) Providers 外部サービスへの DI ハンドル イベントが届いてから付与までの流れは次のとおりです。 Condition/Rule と Action は、新しい種別が必要になったときに対応する実装を追加しておくことで、以降のあらゆるキャンペーン定義から再利用できる仕組みになっています。新規キャンペーンの立ち上げ自体は、すでにカタログに揃っているものの組み合わせで実現できるものであれば、コード変更を伴わずに SQL の INSERT で反映できます。 3 つの軸の中身 ここからは、前述した3 つの軸が Rulebase 基盤の中でそれぞれどう設計されているかを順に見ていきます。 TriggerType — どのようなきっかけで動くか CampaignRules テーブルの TriggerType 列が、各ルールが「どのイベントに反応するか」 を表します。 TriggerType は外部イベントと1対1になるよう定義しており、Pub/Sub から届いたメッセージを内部ドメインのモデルに正規化したうえで、合致する TriggerType を持つルール群を CampaignRules から引き当て、条件マッチングを担う2 段目のハンドラ層に引き渡すところまでが、この軸の責任範囲です。条件評価や副作用はここでは扱わず、後段に切り出しています。 RuleCondition と RuleEvaluator — どのような条件でマッチさせるか CampaignRules に紐づく RuleConditions テーブルには、評価したい条件が1行1 件 並んでいます。各レコードは次の3 つで構成されます。 ConditionType は、その条件がどんな種類の判定をするかを示す分類で、後段でどの ConditionEvaluator にディスパッチするかを決めます。 ConditionParams は、その ConditionType に渡す具体的なパラメータで、種別ごとに必要な引数が違うため、固定スキーマではなく JSON で柔軟に保持しています。 ConditionGroup は、複数の条件を AND と OR で組み合わせるためのグルーピングラベルで、これを使うことで A AND (B OR C) のような複合的な論理式を、フラットな行データで表現できるようにしています。 たとえば「ある期間内で、お客さま属性 B か C のいずれかにマッチしたら成立」 という A AND (B OR C) の条件は、RuleConditions テーブルに次のように 3 行で並びます。 ConditionID ConditionType ConditionGroup グループの意味 A period NULL 単独で AND B user_attribute g1 グループ g1 内で OR C payment_attribute g1 グループ g1 内で OR 評価エンジンの入力は、先に挙げた4 つのレイヤーの情報を1つに束ねた EvaluationData です。ConditionEvaluator 側からは「これさえ読めば判定に必要な値はそろっている」 状態で参照できるようにしてあります。PoC ではここにキャンペーン固有の計算結果も持たせる案を検討していましたが、本実装では「条件評価のフェーズとアクション実行のフェーズで責務を分けるべき」と判断し、 EvaluationData は条件評価に必要な情報だけに絞っています。 評価エンジン本体は、ConditionType ごとの個別判定を担う ConditionEvaluator と、ルール 1 件分の真偽をまとめる RuleEvaluator の 2 段構成です。 下段の ConditionEvaluator は、ConditionType ごとに 1 つずつ実装が用意されており、 EvaluationData を読み取ってその条件 1 件分の真偽を返します。判定は副作用を持たず、外部 API 呼び出しや DB 書き込みは起こりません。 上段の RuleEvaluator は、その上に乗ってルール 1 件分の評価を組み立てます。具体的には、(1) RuleCondition の各行をその ConditionType に応じた ConditionEvaluator に振り分け、(2) 各行が返した真偽を ConditionGroup のセマンティクス (NULL は単独 AND、同じ値どうしは OR、別の値どうしは AND) で AND/OR 合成し、(3) ルール 1 件分の最終的な真偽と、どの条件がどう寄与したかの内訳を返します。返るのは真偽と内訳だけで、計算結果や副作用は含めません。 新しい評価軸が必要になったときは、対応する ConditionEvaluator を実装して ConditionType の enum に登録します。一度カタログに加われば、以降のキャンペーン定義は RuleConditions の 1 行としてその軸を設定値ベースで呼び出せます。 ActionExecutor — 何をするか マッチしたルールに対応する Action を、 ActionType ごとの Executor が実行します。上限計算、ポイント付与ステータスの遷移、ポイント台帳への書き込み、外部へのイベント発行といった副作用は、ここで起こります。Condition / Rule 側は副作用を持たない設計なので、外部に作用するロジックはこの層に集約されます。新しい Action 種別が必要になったときは、対応する Executor を実装して ActionType の enum に登録します。一度カタログに加われば、以降のキャンペーン定義は CampaignRules.ActionType と ActionParams を指定する形で、その Action を汎用的に呼び出せます。 3 軸を支える共通ヘルパー Executor の責務は、Action ごとの副作用そのもの (DB 書き込み、外部 API 呼び出し、PointStatus の遷移など) です。一方で、ポイント計算と Cap (上限) 適用、PointStatus / ProvisionStatus のライフサイクル管理、外部マイクロサービスに渡す冪等キーの生成、設定値テンプレートの展開といった「Action 種別をまたいで毎回必要になる横断的な処理」は、3 軸とは別レイヤーの共通ヘルパーに切り出しています。各 Executor は、自分の Action に応じて必要なヘルパーだけを必要なときに呼び出す形にしてあります。実装したヘルパーはいくつかありますが、代表例を 2 つ挙げます。 PointLifecycleManager (PointStatus / ProvisionStatus のオーケストレーション) ポイント付与にまつわるライフサイクルをまとめて扱うヘルパーです。 お客さまへのポイント付与状態 (PointStatus) と、 会計上の引当状態 (ProvisionStatus) は、それぞれ独立したステートマシンとして表現しています。 上が PointStatus で、 planned で台帳に予定を立て、外部の付与 API が成功すると confirmed 、後処理まで終わると completed に進みます。 planned 直後にキャンセルされた場合は cancelled 、外部呼び出しが失敗した場合は failed に倒れる、というのが主な遷移です。下が ProvisionStatus で、引当が立っていない not_linked から、引当が紐づいた linked を経て、最終的に revoked (引当処理が確定した状態) に遷移するのが主軸です。 旧メルカードパイプラインでは、PointStatus の遷移と ProvisionStatus の遷移がそれぞれポイント付与処理側と引当処理側に分散して実装されていて、両者がどう連動しているかの見通しが悪くなっていました。Rulebase 基盤では、両方のステートマシンを PointLifecycleManager 配下に切り出し、 CompletePoint / ReversePoint / CreateProvision / UpdateProvision / RevokeProvision の各 Action から必要なときに呼び出す形にしてあります。これによって、複数月にまたがる引当が必要なメルカード系のキャンペーンも、引当が不要なシンプルなキャンペーンも、同じ部品の組み合わせでライフサイクルを表現できます。 TemplateExpander (設定値テンプレートの展開) 旧システムには汎用的な文字列テンプレートの仕組みがなく、たとえば会計コードを決済種別ごとに分けて積むような要件が出てくると、会計コードのパターン数だけキャンペーンレコードを別に切るしかありませんでした。本来 1 つのキャンペーンとして扱いたいものをパターン数だけ重複登録する必要があり、運用負荷の原因として積み上がっていた部分です。 これを汎用化するために用意したのが TemplateExpander です。キャンペーン定義のなかに {user_id} / {campaign_id} / {payment_count} といったプレースホルダーを書いておくと、実行時に EvaluationData から取り出した実値に置き換えます。会計コード (たとえば merpay_xxx_campaign-{campaign_id}-{user_status} ) や、ハッシュのシード文字列 ( {user_id}:{campaign_id}:{payment_count} ) などがその利用例です。新しいパターンが必要になったときも、設定マスタ側のテンプレート文字列を差し替えれば、コード変更や追加レコードなしに同一キャンペーンの中で扱えます。 最初の実装事例: コード決済の回数連動キャンペーン ここまでに作った汎用基盤の「入れ物」を使った最初の移行ですが、感覚的には「リスクが低く、運用負荷の高いところ」 から始めたくなります。新しめで蓄積データの少ないものほど移行リスクが下がり、運用負荷が高いほど移行の効果が出やすいからです。当初はメルカリモバイル向けキャンペーンを最初の移行対象に据える予定でした。元々これをアーキテクチャ刷新のきっかけにする案も挙がっていましたし、稼働開始から日が浅く蓄積データが少ない一方で 12 パターンの毎月運用で運用負荷が高い、というまさにスイートスポットの条件に当てはまっていたためです。 ただ、ちょうどそのタイミングで、新規企画として「コード決済の回数連動キャンペーン」 の話が立ち上がります。新規企画であれば、常時稼働している既存パイプラインを止めずに載せ替える、というコストを払わずに、最小構成で「設定値ベースでキャンペーンを組み立てる」 ことの検証ができます。結果として、最初の事例は移行ではなくゼロからの立ち上げに振り直し、本企画のキャンペーンを Rulebase 上で直接立ち上げる形で進めました。既存のメルカリモバイルなどの移行は、先のフェーズに持ち越しています。 この新規企画は、N 回利用するごとに付与が発火するシンプルな仕組みで、お客さまから見ると「使い続けるほど、確実に値引きが返ってくる」体験になります。 これをRulebase上で組むにあたって必要だったのは 累計カウントと周期報酬を扱う ActionType ( COUNT_AND_REWARD )を 1 つだけ追加することだけでした。あとは 既存の ConditionType (期間 / お客さま属性 / 決済属性) の組み合わせで実装できた点です。Rulebase 上で「ルール評価とアクション実行を最大限再利用し、なるべく設定値だけで組み立てる」形を、最初に検証できたケースになりました。 仕組み自体はシンプルです。お客さまのエントリ状況を「お客さま属性条件」で見て、対象決済を「決済属性条件」で絞り込んだうえで、 COUNT_AND_REWARD Executor がカウンタをインクリメントします。カウンタが規定回数に達するとポイントを付与してカウンタをリセットし、これをキャンペーン期間中ずっと繰り返す、という流れです。データとしては、Campaigns 1 行、CampaignRules 1 行、RuleConditions 数行、というシンプルな構造で表現できます (簡略化したイメージ)。 // Campaigns { "campaign_id": "...", "campaign_type": 9, // InfinitePayment "start_at": "2026-04-01", "end_at": "2026-09-30", "metadata": { ... } } // CampaignRules (1 件) { "trigger_type": "PaymentCharge", "action_type": "COUNT_AND_REWARD", "action_params": { "trigger_count": 3, "reward_currency_points": 100 } } // RuleConditions (3 件: 期間 / エントリ状況 / 決済種別) { "condition_type": "period", "params": { ... } } { "condition_type": "user_attribute", "params": { "attribute": "entry_status", "promotion_key": "..." } } { "condition_type": "payment_attribute", "params": { "attribute": "transaction_type", "value": "code_payment" } } 実装上の新規追加は COUNT_AND_REWARD Executor と、カウンタを保持する 2 つのテーブル (現在のカウントとログ) くらいです。他は基盤の汎用部品をそのまま組み合わせて完結しました。 補足として、この回数連動キャンペーンの意義は「単純なキャンペーン派生がやりやすくなった」ことではありません。SQL INSERT で似たキャンペーンを増やすこと自体は、従来の専用パイプライン時代でもそれなりにできていました。Rulebase 基盤で新しく可能になったのは、CampaignType をまたいだ Rule・Action の設定ベース再利用です。一度実装した Condition や Action は、別の CampaignType のキャンペーンからも設定値の組み合わせで呼び出せます。回数連動キャンペーンで使った COUNT_AND_REWARD も、他のキャンペーンが「累積回数で発火する」要件を持てば、コードを書き足さずに設定だけで使い回せます。 同じ仕組みは、動的な条件分岐を 1 つのキャンペーン設定で表現することにも転用できます。たとえばメルカリモバイル向けキャンペーンでは「3 ステータス × 4 データプラン」を 12 個の独立キャンペーン定義として毎月準備していましたが、Rulebase なら属性条件・繰り返し条件・上限条件を組み合わせて 1 つのキャンペーン設定の中で表現できます。「N 回ごとに発火」のような周期的な振る舞いも、 COUNT_AND_REWARD を Executor 側に持つことで汎用パーツとして扱えます。今後の新企画では、既存のルール・アクションの組み合わせで表現できる範囲が広がっているかぎり、ゼロからの実装ではなく「設定値だけで作って試す」ところから入れる、というのが新基盤の最大のメリットです。 余談: ランダムなのに、何度引いても同じ金額 仕様としては 「規定回数に到達したら、複数の候補金額から重み付きランダムで 1 つを引き当てる」 という要件があります。素直に math/rand で抽選してしまうと、Pub/Sub の at-least-once 配信下では同じ PaymentCharge が再配信されたときに 1 回目と 2 回目で別の金額が選ばれてしまうことがあります。PointID ベースの冪等性チェックは通っているのに、2 回目の試行から見える金額が初回付与額とずれる、というケースが起きうる構造です。 そこで抽選は乱数ではなく決定論的ハッシュで行いました。 user_id ・キャンペーン識別子・ payment_count を組み合わせた文字列をシードに、SHA-256 でハッシュ化したものから重み付きで候補を 1 つ選びます。アルゴリズムの概略は次のとおりです (簡略化したイメージ)。 // seed の例: "12345:campaign-abc:3" (user_id : campaign_id : payment_count) seed, _ := expander.Expand(params.DeterministicReward.SeedFormat) h := sha256.Sum256([]byte(seed)) v := binary.BigEndian.Uint64(h[0:8]) // 先頭 8 バイトを uint64 として取り出す var totalWeight uint64 for _, rv := range variations { totalWeight += rv.Weight } target := v % totalWeight // 重みの合計で割った余り (これが候補選択用の値) var cumulative uint64 for i, rv := range variations { cumulative += rv.Weight if target < cumulative { return rv, i // 累積重みで当たった候補を選択 } } お客さまから見た「使うたびに違う金額が返ってくる」体験はそのままに、同じ (user_id, campaign_id, payment_count) の組み合わせに対しては何度 retry が走っても、別プロセスから読まれても、同じ金額が一意に確定します。 そして、シードさえ揃えば抽選結果は確定値なので、Pub/Sub のイベント処理完了を待たずに「今回付与される金額」を計算できます。決済成功直後の API レスポンスやフロントエンド側で「今回は ○ ポイントです」と確定表示を返すことができ、後段で UserPoints が書かれたあとに金額が変わって見える、といった不整合の心配もありません。at-least-once と weighted random を両立させるための仕組みが、お客さまへの早い表示にもそのまま使える形になっています。 QA 環境の改善に助けられた話 今回の QAではいくつかの環境パターンを切り替えて実施する必要があり、工数ボリュームに不安を感じていました。ちょうど良いタイミングで今まで Santa サービスでは実現できていなかった「イベント駆動部分専用の QA 複製環境」をチームメンバーが作りきってくれたことで、並行して QA を実施できるようになり、大変助かりました。その詳細は本連載 8 日目の @mikupo の記事 で詳しく解説されています。 おわりに 今回のリファクタリングでは、「キャンペーン定義をコードからデータへ」 という方針のもと、3 テーブル + 評価エンジンに再構成しました。ローンチ責任との両立で半年寝かせていたリファクタリングの機会を諦めずに掴んで「入れ物」を作り切って本番でキャンペーンをひとつ稼働させたことで、システムに今後の新要件についての受け皿を作ることができました。 ただし、回数連動キャンペーン自体は本記事執筆時点ではローンチしたばかりで、まだ派生キャンペーンの実例はありません。設定だけで派生が立ち上がる世界の本格検証は、これからの新企画を通じて行っていくフェーズです。加えて、メルカード常時還元などの既存のパイプラインもまだ稼働しており、Temporary マップも本番に残ったままです。常時稼働するキャンペーンシステムでは、入れ物を作る難しさよりも、止めずに載せ替えるタイミングと手順の設計こそが、ここから先の本題になります。 今回の記事は以上になります。なにか参考になることがあれば幸いです。 次の記事は orfeon さんの「TiDB / AlloyDB の大規模テーブルを高速にBigQueryni同期するための工夫」です。引き続きお楽しみください。
歴史が証明するMicrosoft 365の強さ〜「Office」という強固な基盤〜 私たちはまず、Microsoft 365が歩んできたエンタープライズITの歴史を学び直しました。 普段からGoogle Workspaceのフラットな世界にいる私にとって、彼らの「重厚なガバナンス」は 少し遠い世界の話のようにも感じられましたが、その背景を知るにつれ認識は大きく変わりました。
はじめに こんにちは! タイミーでPlatform Engineerをしている @MoneyForest です。 2026年6月9日〜10日にニューヨークで開催された Datadog の年次カンファレンス DASH 2026 に参加してきました。弊社からは、MLOpsエンジニアの斎藤が「 How Timee Delivers Day 1 Production Ready LLM Features 」というタイトルで登壇していました。 本記事では、Keynote の全体像、タイミーの登壇セッション、そして Fireside Chat から得たメッセージについてお届けします。 DASH 2026 の全体像 公式のKeynoteの記事 にあるように、まさに「Datadog enables teams to build better with AI」といった内容でした。AI がコードを書くスピードは劇的に速くなったが、それを安全に運用するためのループも同じスピードで回らなければ意味がない。この課題に対し、Datadog は Bits AI というAIエージェント群を中核に据えた新製品群を提示しました。 Keynote Keynote で発表された新プロダクトは、AIエージェントによってループを閉じ、開発を高速化するためのものでした。ループは大きくOps・Dev・AI Agent の3つの軸で整理できます。 全発表の詳細は Datadog 公式の記事 に網羅されているので、ここではループごとの要点に絞って紹介します。 Ops ループ (Detect → Investigate → Remediate) 従来人間が手作業で回していた検知・調査・修復のループを Bits AI で自動化する軸です。 Bits Detection (Preview)はサービストポロジーやデプロイ履歴から何が重要かを推測し、本番が赤くなりそうなときだけ発火するモニターを自動で作成・維持します。大量のフレーキーなモニターリストを置き換えるものです。 Bits Memories (Preview)は Slack でのインシデント対応やポストモーテムから運用上の教訓を学習し、将来の調査に適用します。 Bits Remediation (Preview)は根本原因に対して kubectl コマンド実行やコード修正 PR 作成まで自律的に行います。 Bits Infrastructure Operations (Preview)は OOMKilled や証明書期限切れといった日常的なインフラ問題を自動で検知・修復します。 Dev ループ (Code → Deliver → Evaluate) AI コーディングエージェントが加速する開発スピードに対して、リリースの信頼性を追いつかせる軸です。 Bits Code (GA)は Datadog が問題を検出したあらゆる場所(Error Tracking、APM、Code Security 等)から、本番テレメトリを根拠にした修正 PR を生成します。 Bits Release (Preview)は PR の意図を理解し、「新機能が動くか」「リグレッションがないか」の両面でバリデーションプランを自動生成するリリース検証エージェントです。 Bits Testing Agent (Preview)は URL や自然言語のゴール(例:「黒いサングラスを購入して」)からアプリを自律探索し、セルフヒーリングなテストスイートを生成します。 Agent Console (GA)は Copilot / Cursor / Claude Code 等のコーディングエージェントの利用状況を組織横断で可視化し、非効率なパターンの検出やコストアラートを提供します。 AI Agent ループ (Observe → Evaluate → Experiment → Ship) AI エージェント自体を本番で運用・改善するためのループです。 Agent Observability Patterns (Preview)は、本番の LLM トレースを行動パターンに自動分類します。デモでは15,000件のトレースから想定外の「coordination」パターンが $20,000 のコストを生んでいることを発見し、根本原因分析から修正・検証まで一気通貫で行っていました。 Bits Evals (Preview)はトレース・データセット・プロンプトバージョンを横断して仮説検証やプロンプト改善を自動化します。 Data Observability (GA)はデータパイプライン全体のリネージと異常検知を担います。 Infinite Cardinality Metrics (GA)は課金モデルをカーディナリティベースからメトリクス名+データボリュームベースに変更し、高カーディナリティ環境でのコスト予測可能性を大幅に向上させます。 その他 AI Guard (Limited Availability)はカスタムエージェントとコーディングエージェント双方の入出力をインターセプトし、プロンプトインジェクションやツール悪用をリアルタイムでブロックします。 Runtime Prioritization Engine (Preview)は16,000件の CVE から本番で悪用可能な9件に自動で絞り込み、 Security Analyst (GA)がセキュリティ調査を自動化します。 Journey Monitoring (Preview):Synthetics・RUM・Product Analytics を統合し、ユーザージャーニー単位で可用性・パフォーマンス・コンバージョン率を一つのビューで可視化。「CPU が高い」ではなく「その CPU 高騰でコンバージョンが落ちているか」を見る、ビジネスインパクトへの引き上げが狙い Federated Logs (Preview):Log Explorer から離れずに Databricks 等の外部ストレージを横断クエリ Bits Database Optimization :LLM 生成のクエリ最適化案をシミュレート DB で検証してから PR 化。デモでは500クエリから検証済み30件に絞り、ノイズを9割削減 タイミーの登壇:How Timee Delivers Day 1 Production Ready LLM Features dash.datadoghq.com speakerdeck.com 弊社 MLOps エンジニアの斎藤が、タイミーにおける LLM 機能のプロダクション対応と LLM Gateway の構築について発表しました。内容について要約して紹介します。 背景 タイミーでは LLM がワーカー・クライアント双方の体験を向上させる重要な要素になっています。求人票の自動生成をはじめ、多くのストリームアラインドチームが独立して LLM 機能を活用しており、MLOpsエンジニアがその基盤を支えています。 チェックリストの誕生 最初のプロダクション導入では、LLM の不安定さ(タイムアウト、レイテンシスパイク、レートリミット)を想定した設計を行い、Vertex AI をプラットフォームとして採用しました。この経験から、LLM 機能に求められるプロダクション水準を定義した Production Readiness Checklist を策定しました。一般的なプロダクションの品質基準に加え、LLM 固有のシグナル(フォールバック、モデルレイテンシ、コスト制御など)をカバーするものです。 障害による転機 求人票生成機能の導入後、同一プロバイダー起因で2つの LLM 機能が同時にダウンする障害が発生しました。チェックリスト・モニタリング・ゲートウェイはそれぞれ存在していたものの、採用するかは各チームに依存していたことが根本原因でした。 また、高いスピードで価値を届けるという当然の行動をしていた中で、3人の ML プラットフォームチームが全チームに基準を強制するのは物理的に不可能だったのです。 解決策:LLM Gateway この障害を転機に、全 LLM 呼び出しの共通エントリーポイントとして LLM Gateway を導入しました。Cloud Run 上に構築され、複数の LLM プロバイダーを抽象化しています。 ゲートウェイが提供する価値は3つです。 自由な探索:プロダクトチームがセットアップのオーバーヘッドなしにプロンプトやユースケースを試せる 可観測性:全呼び出しにチーム・機能・環境のメタデータが付与され、トレース・レイテンシ・エラー・フォールバックが一箇所で見える ガバナンス:コスト・使用量・レートリミット・安全性が自動的に強制される これにより、チェックリスト(期待値を定義)× モニタリング(コンプライアンスのエビデンス)× ゲートウェイ(パスの強制)が統一化され、すべてのチームが恩恵を得られる状態になりました。 このセッションは「成功事例ではなく、何が壊れ、何を学び、何ができるかの話」という齋藤の言葉通り、実践的な知見が詰まった内容でした。 Fireside Chat OpenAI や Vercel の幹部を招いた Fireside Chat (対談)が非常に印象的でした。それぞれの視点からエージェント時代のソフトウェア開発について語られましたが、共通するメッセージが浮かび上がってきました。 The New Shape of Engineering(Fireside Chat with Datadog CTO Alexis Lê-Quôc and OpenAI Head of Product and Platform Thibault Sottiaux ) dash.datadoghq.com OpenAI で Codex と API Enterprise を率いる Thibaut との対談では、エージェントが組織にもたらす変化が語られました。 特に印象的だったのは、可観測性の役割が根本的に変わるという点です。エージェントの生産性が人間のレビュー能力を超えるにつれ、「すべてのコードをレビューするのか?」という問いに直面します。システム開発は、コードを一行ずつレビューするのではなく、「症状と振る舞い」で監視する、つまり医者が患者を診断するようなアプローチになるというビジョンが語られました。 また、OpenAI 社内ではフォンブースを使っている人の大半が会議ではなく AI と話していたというエピソードや、Thibaut 自身の Codex の使い方がコーディングから情報の統合と組織の把握へシフトしたという話も、エージェント時代の働き方を象徴していました。 エージェントを正しく使うヒントとして、「生産性の幻想を避ける」(並行して動かしていても、本当に意味のある問題に取り組んでいるか?)と「良い状態を説明する」(同僚に期待値を説明するように、エージェントにも伝えること)が挙げられていたのも実践的でした。 Fireside Chat with Datadog CPO Yanbing Li and Vercel CPO Tom Occhino dash.datadoghq.com Vercel CPO の Tom Occhino との対談では、「意図と実装の距離がほぼゼロに縮まった」という時代認識から出発し、プロダクト開発の変革が議論されました。 特に印象的だったのは、仕事を「2つの波長」として捉える考え方です。 Long Wavelength(基盤作業):コアプラットフォームの品質・信頼性・セキュリティを高める作業。AI を使ってプロセスを加速しても、既存の検証・可観測性・レビューはすべて残る Short Wavelength(グリーンフィールド):誰も依存していない新規領域。AI をエンドツーエンドで使ってどんどんシップする 両方の組み合わせが大事であること、そして Long Wavelength 自体を短くしていくこと、つまりリリースエンジニアリングのエージェント化が次の挑戦として語られていました。 共通メッセージ 両セッションは全く別物ですが、かなり共通する点が多かったのが印象的でした。 「作ること」は安く速くなり、人間の価値は"何を・どう良くするか"の定義に移る OpenAI(Thibaut / The New Shape of Engineering):Thibaut は「良い状態を説明せよ」と語りました。実装が安くなったぶん、エージェントに何を期待するのかを伝えないと、エージェントは勝手に仮定を置いてしまう。難しいプロジェクトの期待値を同僚に説明するのと同じように、成功基準と検証方法を明示することが結果を大きく左右する。 Vercel(Tom / Fireside Chat):Tom は「意図と実装の距離がほぼゼロに縮まった」という時代背景からセッションを始めました。だからこそ PM のコア業務である成功の定義、明確な問題設定、顧客理解はなくなるどころか重要性を増す。仕様が明確であるほどコーディングエージェントの出力は良くなるため。 本番・検証・信頼は"無料ではない" OpenAI:Thibaut は、OpenAIのメインエージェントの全アクションを"元の意図"に照らして検証する Guardian(デュアルエージェント安全システム)を紹介しました。そしてAIエージェントへの信頼は一足飛びには得られない。テストとログへ惜しみなく投資し、実績を積み重ねることで漸進的に築かれていくものだと語りました。 Vercel:Tom は「ソフトウェアの構築はほぼ無料でも、本番は絶対に無料ではない」と強調しました。基盤となる作業(Long Wavelength)では、AI でプロセスを加速しても、可観測性・既存システムへの統合・人間によるレビューはすべて残る、と。 可観測性が検証の中心になる OpenAI:Thibaut は、AIエージェントの生産性が人間のレビュー能力を超えていく中で「すべてのコードをレビューするのか?」と問いを投げかけ、システムは医者が患者を診断するように「症状と振る舞い」で監視する時代になると語りました。過去に発火したアラートを分析してノイズを減らす、アラートのトリアージ自体もエージェントが担い始めています。 Vercel:Tom がユースケースで示したのは、本番から得たインサイトでフィードバックループを閉じる「AIエージェント型インフラ」でした。コアのバイタルを常時監視し、リグレッションが起きれば原因を二分探索し、修正もしくはリバートの PR を自動で出す。コードを一行ずつ追うのではなく、本番の振る舞いから全体の健全性を捉える方向に進んでいます。 まとめ DASH 2026 を通じて感じたのは、「検証、品質、安全性の評価を、個人の規律ではなく経路(path)に作り込む」という考え方が、発表全体を貫いていたことです。 例えばBits Release はリリース検証を、AI Guard はセキュリティ評価を、呼び出し経路に強制的に組み込みます。 この時代におけるプラットフォームエンジニアリングの仕事は基準そのものを作ることではなく、基準が自動で適用される経路(基盤)を作ることだと思いました。

























