
NFT
NFTは「Non-Fungible Token」の略で、直訳すると「非代替性トークン」です。
これはデジタル資産の一種で、ブロックチェーン技術を利用しています。ブロックチェーンは情報を分散して保存する技術で、ビットコインなどの仮想通貨にも使われています。
NFTはデジタルアートや音楽、ゲーム内アイテムなど、デジタルコンテンツを一意に識別するための証明書のようなものです。
例えばあるアーティストがデジタルアートを作成し、それをNFTとして発行すると、そのアート作品は世界で一つだけの存在となります。そして、その所有権はブロックチェーン上に記録され、誰がそのNFTを所有しているかが公開されます。
NFTの魅力はデジタルコンテンツの所有権を証明できることにあります。インターネット上では画像や音楽などのデジタルコンテンツは簡単にコピーできてしまいます。しかしNFTがあれば、そのコンテンツが「オリジナル」であることを証明できます。これにより、アーティストは自分の作品の価値を高め、それを販売することが可能になります。
また、NFTはデジタルコンテンツを売買するための新たな市場を生み出しています。NFT市場ではアーティストが直接自分の作品を販売したり、消費者がその作品を購入したりすることができます。これによりアーティストは創作活動を通じて収入を得る新たな道を開くことができるのです。
79億円や33億円もの価値がついたNFTアートがあることでも話題になりました。
NFTには注意点もあります。NFTの価値は市場の需給によって決まるため、価格は大きく変動します。また、NFTを取引する際には手数料が発生することもあります。そのため、NFTを購入する際には、十分な情報を得てから決定することが重要です。
NFTはデジタルコンテンツの価値を確立し、新たな市場を生み出す可能性を持っています。NFTの動向に注目し、継続的な情報収集をしていきましょう。
これはデジタル資産の一種で、ブロックチェーン技術を利用しています。ブロックチェーンは情報を分散して保存する技術で、ビットコインなどの仮想通貨にも使われています。
NFTはデジタルアートや音楽、ゲーム内アイテムなど、デジタルコンテンツを一意に識別するための証明書のようなものです。
例えばあるアーティストがデジタルアートを作成し、それをNFTとして発行すると、そのアート作品は世界で一つだけの存在となります。そして、その所有権はブロックチェーン上に記録され、誰がそのNFTを所有しているかが公開されます。
NFTの魅力はデジタルコンテンツの所有権を証明できることにあります。インターネット上では画像や音楽などのデジタルコンテンツは簡単にコピーできてしまいます。しかしNFTがあれば、そのコンテンツが「オリジナル」であることを証明できます。これにより、アーティストは自分の作品の価値を高め、それを販売することが可能になります。
また、NFTはデジタルコンテンツを売買するための新たな市場を生み出しています。NFT市場ではアーティストが直接自分の作品を販売したり、消費者がその作品を購入したりすることができます。これによりアーティストは創作活動を通じて収入を得る新たな道を開くことができるのです。
79億円や33億円もの価値がついたNFTアートがあることでも話題になりました。
NFTには注意点もあります。NFTの価値は市場の需給によって決まるため、価格は大きく変動します。また、NFTを取引する際には手数料が発生することもあります。そのため、NFTを購入する際には、十分な情報を得てから決定することが重要です。
NFTはデジタルコンテンツの価値を確立し、新たな市場を生み出す可能性を持っています。NFTの動向に注目し、継続的な情報収集をしていきましょう。
イベント
該当するコンテンツが見つかりませんでした
マガジン

技術ブログ
こんにちは becosuke です。メルカリ NFT と、その上で立ち上げている新規サービスの Backend を担当しています。この記事は「 Merpay & Mercoin Tech Openness Month 2026 」の 20日目の記事です。 この記事では、私たちメルカリ NFT チームがこの4ヶ月ほどで取り組んできた AI-Native な開発について書きます。1月末から、開発のやり方そのものを大きく作り変えてきていて、最終的には人の手をできるだけ介さずにサービスを作り続けられる状態を目指しています。その過程で得た学びを、似たことに取り組もうとしている方に持ち帰ってもらえたらと思っています。 先に結論だけ書いておきます。私たちがリソースを集中させるべきだと判断したのは「正解」そのものではなく、何が正しく何が誤りかの「判断基準」( 注1 )でした。AI を動かすための機構は、これからどんどん外から手に入るようになります。けれど判断基準だけは、その案件ならではの価値観そのものなので、外注できません。だから私たちは、機構を作ることよりも、その上に載せる判断基準を残すことに時間をかけました。なぜそう考えたのか、そして実際に何をやったのかを、順を追って書いていきます。 この案件の位置付け 私たちは、Non-Fungible Token(NFT)の売買を行うマーケットプレイスである メルカリ NFT を既存サービスとして運営しています。いま新しく立ち上げているのは、この既存の メルカリ NFT のコードベースを土台にした新規サービスです。 この案件には、2つの顔があります。1つは、新しいプロダクトの立ち上げであること。もうひとつは、AI-Native な開発のあり方そのものを試す Proof of Concept(PoC)の場であることです。 メルカリには会社全体として AI-Native 化を進める方針があり、社内基盤もすでにしっかり整っています。その方向性には私たちも強く共感しているのですが、今回はあえて一度そこを離れて、前提から組み直すとどうなるかを試してみることにしました。理由は3つあります。 1つ目は、AI に spec から開発を駆動させるには、その spec を解釈するための価値基準や判断の根拠が前提として必要だと考えたからです。これがいちばん大きな理由でした。社内基盤の上に乗ったとしても、AI が「何を基準に判断すべきか」を持たないうちは自走できません。基盤の良し悪しの問題ではなく、その手前で価値基準と過去の判断の根拠を溜める前工程が、私たちにはまだ必要だったということです。だからまずはそこを溜め込む段階を踏んでから、成熟した先で既存の仕組みに接続する、という順序を選びました。 2つ目は、既存の正解の上に乗ると、その枠内での最適化しかできないからです。この案件は新しいプロダクトの立ち上げであると同時に AI-Native 開発の PoC でもあるので、いったん既成の枠を外して前提から組み直すことに意味があります。AI-Native として本当は何が必要なのかを検証したかったので、別のアプローチで組み立て直すことで見えてくるものを優先しました。社内基盤を使うプロジェクトと、別解を試すプロジェクトが並走すれば、会社全体として AI-Native 開発の幅も広がります。 3つ目は、Claude Code 本体の進化に身軽についていきたかったからです。Claude Code には plugin や Agent Teams のような、開発の組み立て方そのものに関わる機能追加が数ヶ月単位で入っていて、AI に開発をさせる足場の作り方が次々に変わっていきます。こうした新機能をすぐ取り込めるよう、 CLAUDE.md と rules、skill、hooks だけの軽い構成で試したかったという事情もあります。標準機能だけで素朴に組むことで、何が効果を生んでいるのかの切り分けもしやすくなります。 念のため書いておくと、この独自路線は社内基盤と対立するものではありません。あくまで今の段階での選択であって、蓄積が成熟したら、社内基盤の考え方を取り込んだり、逆にこの案件で得たやり方を社内のほかの案件でも使えるようにしたり、という双方向の合流を視野に入れています。 この記事でいちばん伝えたいこと:機構は外から、判断基準は自前で 具体例に入る前に、この記事を貫く中心の考え方から書きます。 AI agent は、モデルと、その周りを取り囲む機構(ハーネス)でできていると言われています(Agent = Model + Harness)。このモデルを取り囲む環境・制約・フィードバックを設計する規律は、2026年2月に Mitchell Hashimoto 氏がブログ( 注2 )で "engineer the harness" と名付けて以降、ハーネスエンジニアリングと呼ばれて急速に広まりました。prompt engineering から context engineering、そして harness engineering へ、という発展の系譜です。 ハーネスにはさらに、モデル提供元が出荷する内側ハーネス(Agent SDK など)と、その上に私たちが組み立てる外側ハーネスがあります。Thoughtworks の Birgitta Böckeler は、外側ハーネスを agent の前に指針や制約を渡す Guide / Guardrail(feed-forward)と、agent の出力を観測してフィードバックを返す Sensor(feedback)に分けたうえで、ハーネスの議論は禁止やガードレールの側に偏りがちで、観測の側はむしろ手薄になりやすいと指摘しています。 ハーネスには大きく3つの役割があります。 正解を示す こと 結果を観測する こと 危ない操作を止める こと このどれもが、AI に安心して任せるために欠かせません。私たちはこの偏りに違和感を持ち、3つの役割を等しく支える機構がどこにあるのかを考えました。 ここで私たちが立てた仮説はこうです。この3つを支える機構は、これからプラットフォーム側が整えてくれる。評価ループ、hooks、権限のプロンプト、サンドボックスといった機構は、汎用部品として外から提供される方向に向かっている。実際、危ない操作を事前に止める hooks や権限のプロンプトは Claude Code 本体に組み込まれていて、自前で実装するものから既製品として使うものへと、徐々に移ってきています。だとすれば、その機構づくりは私たちのチームでは「できるだけやらない部分」として切り分けてよい、と考えました。 一方で、3つの役割すべてに共通して必要になるものがあります。何を正解とし、どこからを合格とし、何を危険とするか、それぞれの判断基準です。これは、この案件ならではの価値観そのものなので、外からは提供されません。観測エンジンや停止機構は買えても、その基準は各案件固有の判断の産物で、外注できない。整理すると、三本柱のすべてについて「機構は買える/判断は自前」という構図になります。 機構 判断基準 正解を示す context を供給する足場 何を正解とするか 結果を観測する 評価ループ・eval 基盤 どこからを合格とするか 危険を止める hooks・権限・サンドボックス 何を危険とするか 出どころ プラットフォームが提供(買える) 案件固有(自前で持つしかない) なぜ「判断基準」を集めるのか 個別の正解を一つひとつ集めていくより、判断基準を与えるほうが、応用が効きます。1つの正解は点にすぎませんが、判断基準はその点を生み出す規則です。だから、まだ出会っていない状況に遭遇しても、基準さえあれば AI は自ら考えることができます。 例えばデータの書き換えについて、私たちは「InsertOrUpdate(UPSERT)は使わず、Read で存在を確かめてから INSERT か UPDATE に分岐する」という方針を残しています。UPSERT は全カラムを指定しないと意図しないリセットが起きるためです。この1つの方針が、AI が新しい書き込み処理を書くときの指針にもなり、reviewer が UPSERT の紛れ込みを一次チェックする観点にもなります。Anthropic の言葉を借りれば( 注3 )、正解を示すというのは「モデルの望ましい挙動を最も生みやすいコンテキストを与えること」で、観測するときの基準もそこから派生します。同じ判断基準が、正解を示す側にも、結果を観測する側にも、両方の基盤になっているわけです。 機構と判断基準には、もうひとつ大事な違いがあります。「寿命」が違うということです。Anthropic 自身も、ハーネスの各構成要素は「いまのモデルにこれができない」という前提のうえに置かれているので、その前提は stress test して、要らなくなったら外していく対象だと述べています。( 注4 )実際、ある世代のモデルで必要だった構成要素が、次の世代では不要になったり、別の組合せに置き換わったりしていく例も出てきています。もちろん、test のようにモデルがどれだけ進化しても外せない観測の機構もあるので、すべての機構が一律に失効していくわけではありません。それでも、機構の側には入れ替わりがある一方で、目標と正しい判断基準を与える側は廃れずに残ります。だからこそ判断基準は、特定のツールの設定ファイルに埋め込むのではなく、自分たちのリポジトリに、可搬な形で持っておくべきだと考えました。 ここからは、この考え方を実際の開発でどう形にしたのか、4つの具体例と、それによる変化、それらを社内のほかの案件で共有する方法について書いていきます。どの取り組みも、機構を作り込むことよりも、その上流にある判断基準を残すことに重点を置いた話として読んでもらえると、つながりが見えてくると思います。 1. コードの一貫性:言葉の意味や正しいやり方を1つに固定する 1つ目は、コードの一貫性です。 既存のコードベースを途中から AI-Native に切り替えるために、最初にやったことは大量のリファクタリングでした。人間ならあまり気にしないようなちょっとした言葉のゆれが、AI の判断を狂わせてしまうので、意味を1つに揃えておく必要があります。同じ概念が場所によって違う名前で呼ばれていたり、書き方が場所ごとに違ったりすると、AI はそのたびに周辺から「この案件ではどう書いているか」を推測することになり、出力がぶれます。 そこで、二段階で揃えました。 まず、同じ言葉が複数の意味を指している状態や、その逆をなくして、「1つの概念には1つの名前、1つの名前には1つの意味」というところまで徹底しました。命名は AI がコードベースの全体像を掴むときに最初に頼る手がかりなので、ここがぶれていると、その先のどんな指示や設計書を渡しても精度が落ちてしまいます( 注5 )。 次に、正しいやり方が何通りもあるものについて、どれでやるかを決めてルールとして書き残しました。例えばエラーの扱い、ページネーションの実装、enum の持ち方のように、「正解は複数あるが、案件としてはこの形に揃える」というものをルール化しておく。こうしておくと、今後同じような課題にぶつかったとき、AI は解決の方法を迷わず選べます。前の章で書いた「1つの正解(点)」ではなく「判断基準(規則)」を残す、というのを地でいく作業でした。 この期間、構造を整えながら、差し引きで万単位の行数のコードを減らしました。機能を削ったのではなく、重複や不要な抽象を整理して、同じことをより少ないコードで表すようにした結果です。AI が読まされる量が減り、1つの概念にあたる場所が一箇所に集まることで、AI に渡す文脈はノイズの少ないものになります。コードベースを整えること、それ自体が、AI のための環境整備の土台になる作業だったと考えています。 2. 判断を残す仕組み:spec / design と RDR / ADR 2つ目は、判断を残す仕組みです。 私たちは、仕様書(spec)には「いまどうなっているか」だけを書いて、「なぜそう決めたのか」「どの案を捨てたのか」という判断は別のファイルに分けて残しています。判断の経緯を残しておかないと、しばらく経ってから「なぜここをこう決めたんだっけ」と人も agent も何度も悩み直すことになります。一度悩んだことを文字にしておけば、次の人や AI はその上から考え始められます。 設計(design)の判断には、もともと ADR(Architecture Decision Records)という考え方があり、社内基盤でも使われています。私たちはこれを仕様の側にも持ち込んで、なぜその仕様にしたのかという判断を RDR(Requirements Decision Records)という専用のファイルに残す方針にしました。spec 本体は「現時点の仕様」を表し、RDR は「そこに至るまでの判断の積み重ね」を表す、という役割分担です。 ADR や RDR を「どう書くか」「どんなときに新しいレコードを作るか」といった作法は、 decision-record という skill にまとめてあります。記録そのものは各案件に固有のものなので持ち運べませんが、「記録の作り方」のほうは共通の手順として配ることができます。 判断の経緯や却下した代替案を本文に混ぜると、「今の仕様はどれか」を読み取りにくくなってしまいます。現時点の要求だけを spec に残し、判断の層を RDR に切り出すことで、レビュー対象をノイズから守っています。 AI 側から見ても、この分離には意味があります。エージェントは普段は現状(spec / design)だけを読み、仕様を変えるときにだけ、紐づく RDR / ADR を必要に応じて引きにいきます。こうすると、AI に渡す context には「いまどうあるか」だけが入り、過去の議論や却下案がノイズとして混ざらない状態を保てます。 Git を origin に、Notion を読む場所に ドキュメントの原本はすべて Git に置いていて、Notion には自動で同期したコピーを置いて、読む場所やコメントをつける場所として使っています。 Notion をそのまま origin にすると、ドキュメントがチームの同意なしに誰でも編集できる状態になり、設計や実装との整合性が破綻します。逆に Git を origin にすると、Product Manager(PdM)や Designer に GitHub の Pull Request(PR)レビューを強いることになり、レビュー参加の敷居が上がってしまう。そこで、origin は Git に固定したうえで、Notion 側は「読む場所」「コメントをつける場所」と割り切りました。これで、非エンジニアのレビュアーは使い慣れた Notion でレビューに参加できますし、変更履歴は git log に残ります。 Git から Notion への同期は私たちでスクリプトを書いていますが、両者をつなぐ構成自体はありふれたもので、技術的には難しい話ではありません。むしろ難しかったのは、どちらを origin にするかという方針を最初に決めて、運用のなかでぶれずに守ることのほうでした。 レビューを skill に蓄積する そして、集まったコメントは職種ごとのレビュー用 skill に反映していきます。プロダクトの観点もデザインの観点も、一度もらった指摘によって、次からは AI による一次チェックができるようになります。 コメントを集めるところには、1つ実装上の落とし穴がありました。Notion のコメントは、解決済み(resolved)にするとコメント API からは取得できなくなります。レビューのコメントは指摘が反映されれば解決済みにしていくものなので、コメント API だけを見ていると、いちばん学びになる「対応済みの指摘」がごっそり抜け落ちてしまう。そこで私たちは、コメントを集めるときはコメント API ではなく履歴 API のほうから取得するようにしています。 例えばプロダクトのレビュー用 skill は、いつも的確な指摘をくれるメンバーのコメントから育てています。他の案件からはなかなかレビューをお願いしづらい立場の方でも、その観点を AI がいつでも一次チェックとして返してくれるようになるので、価値が大きいと感じています。レビューがその場限りで消えずに AI の中へ積み上がっていくのも、判断基準を残すことの1つの形です。 spec → design → plan → issue のパイプライン ここまで書いてきた仕様書や設計書、それにレビューの skill は、つなげて連続的に動かすこともできます。一つひとつを手で進めても十分機能しますが、つなげておくと、仕様書の変更がトリガーとなって実装まで自走させることもできます。 私たちは3つのドキュメントを使い分けています。 ドキュメント 役割 対応する skill spec 仕様書 spec-review design 設計書 design plan 手順書 plan それぞれに専用の skill があって、新しい仕様書が入ってくると、まず spec-review skill がその内容をレビューします。良さそうだと判断されたら、次に design skill で設計書をつくり、そこから plan skill で実装計画を立てます。plan skill は、もともとある plan モードを少し拡張したもので、立てた計画をファイルとしてリポジトリの中に残し、その手順の一つひとつに進捗のチェックがつくようになっています。リポジトリにファイルとして残しておくことで、session をまたいでも、別のメンバーが見ても、その計画と進捗をそのまま読める状態になります。 最後の issue skill だけは少し毛色が違っていて、ドキュメントというより、できあがった計画を実装処理へ渡すための受け渡し役です。issue skill で GitHub issue を用意し、そこに plan ファイルへの参照を入れておくと、GitHub Actions がそれを拾って実装を進め、終わると PR を出してくれます。つまり、仕様書の変更がトリガーとなって、ここまで一気通貫で進められるというわけです。 このパイプラインの土台になっているのは、それぞれの skill のなかに書き留めてきた判断基準です。spec-review が見るべき観点も、design や plan の組み立て方も、すべて私たちが少しずつ言葉にしてきたものです。パイプラインの組み立て自体は Claude Code の標準機能と GitHub Actions の組み合わせなので、目新しさはありません。新しいのは、機構の側ではなく、その上に積み上げた基準のほうです。 3. 検証ループ:推論ではなく、決定論を用意する 3つ目は、テストの話です。AI に開発を任せるとき、「ちゃんと動いていることをどう確かめるか」は、前章で挙げた三本柱のうち観測の役割にあたります。 ブラウザを動かして操作をひと通り確かめる、通しの End-to-End(E2E)テストを思い浮かべてください。AI にこれをやらせる方法として、Claude のように、その場でブラウザを直接操作してもらうやり方があります。ただし、この方法は毎回 AI の推論が入るので、結果がゆれますし、実行のたびにトークンも消費します。テストの本来の目的は「同じ入力なら同じ結果になること」を確かめることなのに、確かめる側がゆれてしまうと、本末転倒です。 そこで私たちは、AI を活用するのはテストの定義を作るところまでにして、実行のほうは推論を挟まない確定的なやり方にしました。AI に新しい機能の操作手順と期待される結果を書いてもらい、書き終わったら、その後の実行ループからは AI を抜く、という分け方です。こうすると、ゆれる可能性のある推論から決定論に変わって、同じ入力なら必ず同じ結果になります。Continuous Integration(CI)でも手元でも同じスクリプトが同じように走るので、差分が出れば、それは AI のゆらぎではなく、プロダクトに本当の変化があったサインだと、はっきり切り分けられます。 実際に、メルカリ NFT の主要なフローを Playwright で組みました。出品、購入、オファー、承諾、買取、配送といった一連の流れを、売り手と買い手を切り替えながら通しで実行します。これを「regression test を実施して」の一言で AI が全シナリオを走らせる状態にしてあるので、コマンドを覚える必要はありません。 確かめ方そのものを「正解」として固定するというのも、判断基準を残すことの1つの形です。AI に任せる範囲を広げるほど、人の代わりに結果を観測する機構は大事になります。そのときに、観測する側まで AI に任せきってしまうと、ゆらぐ判定でゆらぐ実装を確かめるという不安定な構造になってしまいます。確定的に動く部品を観測の側に据えることで、AI に任せる範囲を安心して広げることができるようになりました。 4. 個人情報(PII)の多層防御 4つ目は、セキュリティ、とくに個人情報の扱いです。社内でも注目されているテーマです。 commit や PR にうっかり個人情報が混入することは、人でも AI でも起こりえます。私たちは commit も PR も、それに Jira チケットの作成も AI にやってもらっているので、AI に開発を広く任せるのであれば、個人情報の保護にも AI を活用するのは自然な流れだと考えました。 そこで、一連の流れに対して個人情報を保護する機構を、三段の防御として載せることにしました。 三段にした意図は、どこか一段が落ちても別の段がカバーする構成にしたかったからです。一段目をすり抜けても二段目で止まり、それも越えてしまったら三段目で後始末できる。完全にミスをなくすことを期待するのではなく、ミスは起こる前提で複数の網を重ねる、というスタンスです。 もうひとつ、skill によるコメントなどで個人情報を引用するときは、先頭の数文字だけ残してあとはマスキングするルールにしています。AI のコメントは、PR の本文に残ったり、コミットメッセージや別ドキュメントに引用されたりと、思わぬ場所に流れていきます。該当箇所が特定できる程度に頭を残してそれ以外を伏せておけば、AI があとでどこかに書き残すときにも、本物の個人情報が混入することはありません。 この例では、機構(hooks や CI のスキャン、復旧手順)も判断基準(何を個人情報とみなすか)も、どちらもプラットフォームでなく私たち側で用意したものです。それでも、機構と判断基準を別物として扱い、判断基準を機構の上流に置く、という構図は変わりません。両者を区別して別々に扱う、という構図自体は崩れないということを、この例はあらためて思い出させてくれました。 どれくらい変わったか ここまでに紹介した4つの取り組みは、実際にチームの開発にどんな変化をもたらしたのか。指標としては定量的に見られるものと、日々の運用感覚として感じる定性的なものの両面で、いくつかわかりやすい変化が出てきました。 コードの一貫性を整え、開発の効率を上げる skill を入れていった結果、チーム全体の PR の数そのものが増えました。しかも、この案件は1月末にいったん開発チームを解散していて、それ以降はエンジニアが2人、プロダクトとデザインが1人ずつ、という少人数の体制です。チーム全体の流量が増えているうえに人数が少ないので、1人あたりで見れば何倍にもなっている計算です。 これは AI を導入したから自動的に増えた、というよりは、AI が力を出せるようにコードや判断基準を先に整えてきた結果だと考えています。漫然と AI を導入するだけでは、同じ結果にはならなかったはずです。 もうひとつ、PR の中身についても触れておきます。開発全体の PR 流量が増えている一方で、修正系の PR の割合は抑えられています。新機能や改善の PR の伸びが、修正系の伸びを上回っている、ということです。これは PR の乱造ではなく、品質を上げた上で流量も増えているという、非常に大きな成果だと思っています。 それとは別に、アラートの調査を AI に任せるための skill(監視基盤を直接触れるようにしたもの)も整備しました。アラートの URL を渡すだけで原因を調べて修正までまとめてやってくれるようになりました。日々の運用感覚としても大きい変化でした。 共通と固有を分ける:ほかの案件に持っていくために ここまで積み上げてきた仕組みは、この案件だけで閉じてしまうのはもったいないので、社内のほかの案件でも使えるようにしておきたいと考えました。そのためにやったのは、どこが共通で、どこがこの案件固有のものかを、はっきり分けておくことです。 この「共通と固有を分ける」やり方は、たった1つの小さな判断から始まりました。社内共通のフレームワークである monorail をアップグレードしたときのことです。monorail のチームは、アップグレードの共通手順を monorail-upgrade という skill で提供してくれています。ただ、私たちの案件には、それだけでは足りない固有の手順がありました。そこで、その名前のうしろに -local をつけた monorail-upgrade-local という skill を作って、固有の手順をそちらに置きました。この小さな切り分けが、いまの形の原型になりました。 私たちのプロダクトは、Backend と Frontend が別々のリポジトリに分かれていますが、共通で使える skill は、1つの plugin にまとめて配っています。そして、それぞれのリポジトリに固有の部分は、その plugin を上書きする -local な skill に入れています。こうしておくと、片方のリポジトリで skill を改善するともう片方にも横展開しやすくなりますし、skill の名前を見ただけで「どこからが共通で、どこからが案件固有か」が判別できます。 このやり方は、Claude Code の CLAUDE.md と CLAUDE.local.md の考え方を応用したものです。plugin を案件ごとに上書きする仕組みは、Claude Code に機能として用意されているわけではなく、Feature Request として GitHub に issue が出ている段階です。そこで私たちは、 -local という名前のつけ方を決めて、その規約だけで共通と固有の境界を表すことにしました。人によってはすでに自然にやっていることかもしれませんが、これを暗黙のままにせず、命名ルールとして明示しています。 大きな仕組みを最初から設計する必要はありませんでした。小さな判断を、次の人がそのまま使える形で残していけば、それが積み重なって、いつでも広げられる土台になります。 補:個人的な思想 ここからの話は、これまでの章とは少し性質が違います。この案件の特性から導かれた話というよりは、私個人の運用観の話だと思って読んでもらえると、はずれが少ないと思います。同じことをやろうとしている人が、これと違うやり方を選んでもなんら問題ありません。今回の記事に紛れ込ませているのは、この4ヶ月のなかで自分の中にまとまってきた考え方を、せっかくなので書き残しておきたかったからです。 専門知識は skill に集め、agent は薄いラッパーに保つ Claude Code には、知識や手順をまとめておく skill と、独立した context を持って動かせる subagent という2つの仕組みがあります。コミュニティの best practice を眺めていると、機能領域ごとに専用の subagent を作って、そこに専門知識を持たせていくやり方をよく見かけます。私が選んでいるのはこれとは違うルートで、専門知識は skill にまとめて、agent はそれを呼ぶための薄いラッパーに留める という分け方です。 agent と skill は対立する選択肢ではないと思っています。agent を作ってもよいのですが、agent には「この場面ではこの skill を、次にこの skill を呼ぶ」というルーティング情報だけ書く、と考えています。判断基準や手順、ドメイン知識といった本体は、すべて skill 側に書く。こうしておくと、同じ判断基準が複数の agent に重複して埋め込まれることがなくなりますし、skill を更新すれば、それを呼ぶすべての経路に反映されます。 例えば Anthropic は、長時間動く agent harness の設計について、実装する generator agent とテストする evaluator agent を別の agent として分離することを推奨(注4)しています。生成する agent に自分の仕事を批判させるよりも、別の agent を評価役として厳しくチューンするほうがやりやすい、という理屈です。agent を切るとしたら、こういうメイン context からの隔離と独立した判断、それに伴う権限スコープが単独で必要なときが良さそうです。その場合も専門知識を埋め込まずに、必要な skill 群を呼ぶ薄いラッパーとして組みます。agent の固有価値は隔離・独立した判断・権限スコープであって専門知識そのものではない、というのが私の見立てです。専門知識は skill 側に集めたほうが、再利用しやすく、知識の重複も起こりません。 rewind ではなく、新しい session を fork する Claude Code には、過去のやり取りを巻き戻して途中の状態から再開できる rewind という機能があります。便利な機能なのですが、私は意識的にあまり使わないようにしています。理由は単純で、rewind は context の rebase に近いところがあって、やってきた流れの一部が history から消える形になるからです。 これは好みの問題で、git の rebase が悪いと言いたいわけではありません。git log がきれいになるメリットと、流れがそのまま残るメリットには、どちらにも価値があると思います。ただ私自身は流れを残しておきたいほうで、後から「あのとき何を試して、何で失敗したか」を辿れるほうに価値を感じます。 やり直したいときは、rewind の代わりに新しい session を fork するようにしています。レビューしたいときも、進捗の区切りごとに分けたいときも、その都度別 session を立てます。session を分けたほうが、それぞれの context を小さく保てるので、結果的に動きも安定すると感じています。 plan ファイルと TODO.local.md が session 間のバトンになる session を多めに切る運用には、1つ前提があります。進捗を別 session に渡せる形で残しておく必要があるということです。これを担っているのが plan ファイルと、それからもうひとつ、 TODO.local.md というファイルです。 plan ファイルのほうは、前章で書いた spec → design → plan → issue のパイプラインで出てくる、あの plan です。手順の一つひとつに進捗のチェックがついていて、ファイルとしてリポジトリに残っているので、別の session を立てても、最後にどこまで終わっていたかをそのファイル 1 枚で引き継げます( 注6 )。同じことが git worktree を切って並列で動かしているときにも当てはまります。worktree A で進めていた作業を worktree B から続けることもできますし、agent 間でタスクを引き渡すときも、plan ファイルの該当箇所を指せば、お互いのなかで同じ進捗の理解が再現できます。 TODO.local.md のほうは、個人レベルの todo を扱うためのファイルです。チケット化するほどではない、あるいはチケット化する前段階のもの、例えば「あとで気が向いたら直したい」「明日の朝いちで手を付ける」といったレベルの個人 todo を、ここに置いています。これを管理するための todo skill も用意していて、追加、完了マーク、一覧表示、コード内 TODO の集約( 注7 )などができます。チケットほど重くなく、その場の独り言で消えてしまわない、中間地点のタスク管理に意味があります。 plan ファイルと TODO.local.md の2つで、チケットに上げるほどではない作業の進捗が、session や worktree、agent をまたいで運べるようになっています。これが、session を多めに切る私の運用を支える土台です。 中心の軸とのつながり ここまで書いたのは私の運用観なので、章の頭で断ったとおり、違うやり方を選んでもなんら問題ありません。ただ、最初の章で書いた「機構は外から、判断基準は自前で」という軸を頭に置きながら振り返ると、1つ気付くことがあります。 skill と plan ファイルと TODO.local.md は、判断基準が宿る側にあるということです。判断基準や手順、進捗の文脈を、可搬なファイルとして残しているからです。一方の agent や rewind、session の制御は機構の側で、判断基準そのものを抱え込ませる対象ではない、と私は考えています。 機構と判断基準を分けて、判断基準を機構の上流に置く、という構図を貫こうとすると、自然にこういう運用に寄っていくのではないかと思います。 まとめ AI を動かす機構はこれからどんどん外から手に入るようになるので、私たちの仕組み化は、その上に載せる判断基準を残すことに集中してきました。 何を正解とし、どこからを合格とし、何を危険とするか コードの一貫性も、判断の記録も、テストの固定も、個人情報の保護も、すべて「判断基準を、価値観と理由ごと残していく」という同じ考え方でつながっている 観測や停止の機構はモデルが進化するにつれて入れ替わったり、外せたりする部分がある。一方、判断基準は廃れにくい資産なので、特定のツールの設定ファイルに埋めるのではなく、自分たちのリポジトリに可搬な形で持っておく これらが、この4ヶ月の試行錯誤から得た学びでした。 結果が出るに越したことはないのですが、まだ誰も正解が分かっていない領域なので、いまは結果そのものよりも、改善していける過程に重きを置く段階だと思っています。同じようなことに取り組んでいる方、真似してみたい方がいれば、ぜひお話ししたいです。最後まで読んでいただき、ありがとうございました。 脚注 注1 ここでいう「判断基準」とは、ドメイン知識と開発ルールを併せたものに近いと、いまは捉えています。ドメイン知識のほうには、その業務を成り立たせるための知識だけでなく、そのドメインで何に重きを置くかという価値観も含まれます。 注2 Mitchell Hashimoto, "My AI Adoption Journey"(2026-02) https://mitchellh.com/writing/my-ai-adoption-journey 注3 Anthropic, "Effective context engineering for AI agents"(2025-09) https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents 注4 Anthropic, "Harness design for long-running application development"(2026-03, Prithvi Rajasekaran) https://www.anthropic.com/engineering/harness-design-long-running-apps 注5 これは人間も間違える原因になるので、AI を使わないとしても徹底したほうが良いと思っています。今回は AI-Native の話なので、本筋ではありませんが。 注6 Justin Young, "Effective harnesses for long-running agents"(2025-11) https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents session をまたぐ継続性を artifact(この場合はテキストファイル)で確保するという趣旨が該当します。 注7 これは案件内で共通のものなので TODO.md にまとめられています。IDE を常に開いていた頃なら、こういった TODO は自動的に収集してきてくれましたが、最近は Claude Code だけで済んでしまうので、こういったものが必要になりました。
こんにちは。メルペイ Growth Platform チームでエンジニアリングマネージャーをしている @yo-gawa です。この記事は Merpay & Mercoin Tech Openness Month 2026 の 19 日目の記事です。 私は 2018 年にメルペイに入社し、メルペイクーポンや決済に応じたポイント還元の仕組みなど、メルペイのキャンペーンを支える基盤の開発・運用に携わってきました。 【書き起こし】メルペイのキャンペーンの舞台裏 〜Growthを支える仕組み〜 – 小川 芳樹【Merpay Tech Fest 2021】 | メルカリエンジニアリング 現在は、メルカリグループ全体のグロース基盤のエンジニアリングを統括しています。 本記事では、私の視点からメルカリグループのグロース基盤の歴史と、どのようにチームが拡大してきたのかをお伝えします。 Growth Platform の立ち上げ メルカリグループでは、ポイントやクーポンを用いたマーケティング/キャンペーンが非常に活発に行われています。既存のお客さま向けの施策に加え、メルカリハロやメルカリモバイルといった新規事業のグロースにも活用されています。また、お客さまとのコミュニケーションチャネルとして、アプリ内バナー、メール、Push 通知、「あなたへのお知らせ」等も併せて使われています。Growth Platform チームは、こうしたグロース活動を支える基盤・仕組みづくりを担うチームとして機能しています。 「Growth Platform」というチーム名称が生まれたのは、ちょうど 5 年前の 2021 年 7 月でした。 「Growth UXチーム」が「Growth Platformチーム」に名称変更! 組織改編で見えたPMの役割やチームの変化 | mercan (メルカン) 上記の記事にもある通り、「共通する機能・基盤を横断的に見て、システム開発や運用の観点から下支えする」というミッションのもと、さまざまな取り組みを進めてきました。 Growth Platform というチームが生まれる前、2019 年のメルペイサービス開始直後は、より多くのお客さまにメルペイを使っていただくため、決済に絡めたクーポンやポイント還元キャンペーン施策を次々と打ち出していました。そのスピード感を支えるべく、独自にキャンペーン基盤を開発してきました。 一方で、マーケットプレイス事業にも同様にキャンペーンや Customer Relationship Management(CRM)を実行する基盤が存在していましたが、長いメルカリの歴史の中で生み出されてきた PHP 製ツールには多くの負債があり、メンテナンス性の観点からも、オペレーション事故が起きやすい構造になっていました。メルペイ側でも、それらのツールを利用することにリスクを感じていました。 そこで、メルカリとメルペイのエンジニア間で実務的なコラボレーションをスタートし、古いツールを廃止して、新たに Engagement Platform (EGP) という内製マーケティングエンジンを共同開発する方向性で合意しました。 その後、2023 年 1 月よりメルペイ内に統合組織を作り、「JR (Japan Region) Growth Platform」として、一丸となった開発体制をスタートさせました。詳細は当時のエンジニアリング統括であった keigoand の記事にまとまっています。 Merging Teams for a Growth Platform | Mercari Engineering 記事にもありますが、当初はそれぞれのチームで開発の進め方が大きく異なりました。メルペイが比較的小規模で立ち上げを進めた背景もあり、独自のオペレーションツールやミドルウェアがあり、エンジニアリングカルチャーも異なっていました。また、メルペイは金融事業を担っていることもあり、システム品質の担保も重要です。リリースプロセスの厳格化や、QAなどの各種基準への準拠などについて認識を合わせながら開発を進めることは苦労も伴いましたが、チームのケイパビリティは格段に向上しました。 グループの拡大とともに 先述した通り、Growth Platform チームはメルペイに所属しながらも、メルカリグループ全体を支えるチームです。 この数年、メルカリでは複数の新規事業が立ち上がりました。 メルカリShops メルコイン メルカリNFT メルカリハロ メルカリモバイル 越境ビジネス メルカリグローバルアプリ etc これらのビジネスやプロダクトの Product-Market Fit を支えるべく、Growth Platform ではさまざまな開発を行ってきました。例えば、新しいサービス画面上でのキャンペーンバナー表示や訴求、新規のお客さまへのクーポン/ポイント進呈、利用実績に応じたロイヤリティプログラムなどは、一見共通でありつつも、それぞれのサービスのデータを基盤に統合し、マーケティングチームが柔軟かつ高速にキャンペーンを打ち出せる仕組みが必要です。また、グローバル展開においては、各地域の言語や、クーポンにおける通貨の違いへの対応などもスコープになります。 さらに(一部例外はありますが)、メルカリのサービスは 1 つのメルカリアプリ上で動いています。起動直後に表示される「ホーム画面」や、マーケットプレイスにおける「商品詳細画面」「取引画面」でのキャンペーン訴求は、お客さまにサービス認知を獲得するうえで非常に重要です。そのような訴求エリアが限られる中で、どのサービスがそれぞれのお客さまにとって有用なキャンペーンであるのか、コミュニケーションチャネルがノイジーになりすぎないのか、といった最適化にも Machine Learning チームが取り組んでいます。 組織の拡大 事業の拡大に対応するため、Growth Platform 組織も拡大を続けてきました。 2022 年 6 月にインド・ベンガルールに開発拠点として Mercari India を設立して以降、設立当初から一緒に開発を進めています。当初はクーポンドメインを共同で開発するところからスタートしました。新たなビジネスに対応したクーポン機能の開発を進めながら、mercari-api という PHP 製モノリスからマイクロサービスへのマイグレーションという大規模プロジェクトも自律的に推進しています。 Migrating Coupons from Monolith to Microservice – Mercari India 近年ではクーポンドメインだけでなく CRM ドメインでも開発体制を拡大し、Backend、Frontend、Mobileのエンジニアが一体となってインド拠点のみで機能開発を完結するような事例も出てきています。 現在は日本とインドのエンジニアが約2:1の割合で協働しています。また、日本とインドのエンジニアリングマネージャー同士、密に連携しながら開発を推進しています。 Growth Platform の全体ミーティングでは、知見共有を含めた交流をエンジニア同士で積極的に行っています。加えて、定期的にインドオフィスへ出張したり、インドのエンジニアが東京オフィスを訪問するタイミングに合わせて交流を深めたりしています。 (インドオフィスに設置されているオブジェへのサイン) 一方で、使用言語の壁は依然として課題です。メルペイは従来、日本語中心でコミュニケーションしてきましたが、元マーケットプレイスのメンバーやインドオフィスのメンバーは英語中心です。Growth Platform チームの全体ミーティングは英語で行う一方、メルペイ全体のミーティングは日本語で実施しているといった状況下で、GOT(通訳)チームのサポートも借りながら、日本語話者・英語話者の双方が歩み寄ってコミュニケーションを進めています。 次なるチャレンジへ Growth Platform チームの成熟は、メルカリグループのグロースとともにありました。 EGP は、さまざまな機能開発を経て大きな進化を遂げています。 EGP – Mercari’s CRM Platform: Built Once, Powering Many | mercari GEARS 2025 AI と作る HTML ベースの LP エディタ EGP Code を内製した理由 | メルカリエンジニアリング また、メルペイに特化した基盤である Santa サービスも進化を続けています。 メルペイのキャンペーン基盤をルールベース汎用システムに書き直し、Otoku Revolutionするまでの話 | メルカリエンジニアリング 基盤開発フェーズは一巡したと捉えていますが、ビジネスを支える基盤としてはまだ道半ばにあります。特に、ビジネスの要求に対するアジリティとシステムの信頼性向上は継続的な課題です。 そこで今後は、グループそれぞれのビジネスグロースをより直接的に支えつつ、守りも固められる開発体制をつくるために、Growth Platform 体制としては一区切りとし、組織を CRMチーム と Incentiveチーム に分け、それぞれのミッションを担っていきます。 CRM チームは、マーケットプレイスをはじめとする各事業の成長を後押しするため、プロダクトと密に連携しながら、EGP を活用したお客さま向けコミュニケーション機能の開発・改善を進めていきます。 Incentive チームは、メルペイ Payment & Customer Platform の一員として、インセンティブを汎用的かつ安全に取り扱える仕組みづくりを推進していきます。 チームは分かれますが、メルカリグループ全体で活用される共通 Foundation として、連携は継続していきます。 まとめ ここまで、Growth Platform のプロダクトやチームの拡大について簡単にご紹介しました。プロダクトや技術の詳細は、本エンジニアリングブログやカンファレンスでメンバーが紹介していますので、そちらもぜひご覧ください。 これからもメルカリグループの可能性を広げるための仕組みづくりを追求していきます。 次の記事は abcdefujiさん と becosukeさんです。引き続きお楽しみください。
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同期するための工夫」です。引き続きお楽しみください。
動画
該当するコンテンツが見つかりませんでした






