こんにちは。メルペイの Balance Team で Tech Lead をしている @kobaryo です。この記事は「Merpay & Mercoin Tech Openness Month 2026」の18日目の記事です。
はじめに
Balance Team では お客さまの残高やポイントを扱うマイクロサービスをはじめ、債権を取り扱う Debt Service も管理しています。先日公開された @imamu さんの「事業者請求払いのための与信管理マイクロサービスの設計」で、与信を扱う Credit Service を中心に事業者請求払いの設計が紹介されましたが、本記事ではその中で取り上げられた「債権を任意の軸で集計する仕組み」を Debt Service 上でどのように表現したのかについて紹介します。
事業者請求払いでは、与信管理のために「与信枠ごとの未返済債権」を高速に取り出したい、また請求のために「請求先ごとの債権の一覧」を把握したい、という2つのニーズが生まれます。お客さまや事業者ごとの残債を管理する既存のテーブル DebtAccount はお客さまや事業者を表す ID という単一の軸に縛られており、この2つの独立した軸を同時に管理できませんでした。この壁を越えるために、「DebtAccount は債権の操作ログ DebtLog を集計したビューである」という発想から DebtAccountView を設計しました。1つの DebtLog を異なる軸で集計された複数のビューに畳み込むことで、与信枠軸と請求先軸を独立して管理でき、将来の新しい集計軸も最小限の変更で追加できます。
Debt Service が管理する「債権」とはなにか
事業者請求払いでは、我々が取引のたびに代金を立て替え、後からまとめて事業者に請求します。この立替金の記録が債権(Debt)です。Debt Service はメルペイの中で債権を一元管理するマイクロサービスで、事業者向けのみでなくお客さま向けのメルペイのクレジットなど、複数のサービスから呼ばれる下位レイヤーとして機能します。
各 Debt は「誰の(CustomerId)」「何の種別の(DebtType)」「いくらの(Amount)」立て替えかを保持します。DebtType は決済の種別や精算方法を区別するための分類で、お客さま向けの月次清算を表すものや事業者向けの請求書払いを表すものなど複数の種別が存在します。
個別の Debt が増えていくと、「ある事業者の現時点での残債はいくらか」を素早く知りたい場面が増えます。Debt を1件ずつスキャンして合算するアプローチは、Debt の件数に比例して計算コストが大きくなるため、取引のたびに現在の残債が与信上限を超えないかをリアルタイムで確認するには向きません。そのために DebtAccount というテーブルでお客さまや事業者ごと、かつ種別ごとに残債を保持しています。Debt の作成や返済が発生するたびに DebtLog(債権の操作ログ)が記録され、それに応じて DebtAccount の Amount が更新される仕組みです。

「1口座1軸」という壁
DebtAccount の設計はシンプルです。Unique key (CustomerId, DebtType) によって、「この事業者の、この種別の残債はいくらか」という問いに即座に答えられます。お客さま向けのメルペイのクレジットなど既存のユースケースではこれで十分対応できました。
請求書払いプロジェクトでは、この設計に2つの新しい要件が加わりました。1つは与信管理の要件です。詳細な説明は上述の @imamu さんの記事に委ねるのですが、今回店舗や部門ごとなど1事業者が複数の与信枠を持てる設計を目指しています。そのため、与信枠を扱う Credit Service は決済フローの中で「ある与信枠に紐づく残債」をリアルタイムに取得できなければなりません。もう1つは請求の要件です。Invoice Service は請求先の軸でまとめた債権から請求書を発行するため、「ある請求先に紐づく債権の一覧」を取得できなければなりません。与信の軸(与信枠)と請求の軸(請求先)は完全に独立しており、1つの債権が両方に同時に紐づきます。またこれらの軸はどちらも CustomerId すなわち既存の事業者IDと独立しているため、既存の DebtAccount を用いて集計することはできません。
2つの要件はそれぞれ性質が異なります。与信計算は決済フローの中でリアルタイムで行われ、応答時間が Debt の件数に依存してはならないため、与信枠の軸でも既存の DebtAccount と同じ「1行参照で残債がわかる」事前集計が必要です。請求の要件は残債の集計ではなく、明細の作成や債権の返済のためにどの債権がどの請求先に属するかという紐づけを保持しておくことです。ただし後々事業者向けに請求額を確認できるダッシュボードを提供したいなど、請求先軸でも事前集計する必要が出てくるかもしれません。またこれらに追加して3軸目がこの先登場しないとも限りません。このように考えた場合、我々に必要なものは多軸で残債を集計でき、かつその軸と債権を紐付けておく汎用的な仕組みでした。
1つの操作を複数の軸で同時に集計する DebtAccountView
この仕組みを実現するヒントは、DebtAccount の性質そのものにありました。DebtAccount は「口座」という名前のとおり実体を持つ台帳のように見えます。しかし、すべての事実は DebtLog に記録されており、DebtAccount はその DebtLog を (CustomerId, DebtType) 軸で集計し、現時点の残債を1行に集約したビューです。この捉え方に立つと、「同じ DebtLog を別の軸で畳み込んだビューが複数あってもよい」という発想が生まれます。これが DebtAccountView の設計の直感的な出発点です。
口座を分割するのではなく、集計の軸を並列に増やすというのが、設計の方向です。DebtLog は従来 DebtAccount という1つの集計ビューだけを更新していました。ここで、1つの DebtLog が既存の DebtAccount に加えて任意の数のビューを同時に更新できるようにしました。

各 DebtAccountView は既存の DebtAccount と同じ物理構造を持つ集計テーブルです。DebtAccount の設計思想をそのまま踏襲しているため、DebtAccountView 専用の新しい運用パターンを考える必要はありません。従来「1 DebtLog が1 DebtAccount を更新する」ところを「1 DebtLog が1 DebtAccount + N 個の DebtAccountView を更新する」に拡張した形です。
3つのテーブルが作る台帳の構造
この仕組みを支えるのが3つのテーブルです。DebtAccountViews(集計した残債)、DebtAccountViewItems(Debt と DebtAccountView の所属関係)、DebtAccountViewLogs(Debt Account View の変動履歴)がそれぞれの役割を担います。この機能に関わる主要な3テーブルのスキーマを示します。
-- 集計した現在の残債
CREATE TABLE DebtAccountViews (
DebtAccountViewId STRING(100) NOT NULL,
ViewKeyType INT64 NOT NULL, -- 1=請求先 ID, 2=与信付与先 ID, ...
ViewKeyId STRING(MAX) NOT NULL, -- 指定した ViewKeyType の中の ID
CustomerId INT64 NOT NULL,
DebtType INT64 NOT NULL,
Amount INT64 NOT NULL,
) PRIMARY KEY(DebtAccountViewId);
CREATE UNIQUE INDEX DebtAccountViewsByViewKey
ON DebtAccountViews(ViewKeyType, ViewKeyId, DebtType, CustomerId);
-- どの Debt がどの DebtAccountView に属するか(請求書作成時に使用)
CREATE TABLE DebtAccountViewItems (
DebtAccountViewId STRING(100) NOT NULL,
DebtId STRING(100) NOT NULL,
) PRIMARY KEY(DebtAccountViewId, DebtId);
-- 残高変動の履歴
CREATE TABLE DebtAccountViewLogs (
DebtAccountViewLogId STRING(100) NOT NULL,
DebtAccountViewId STRING(100) NOT NULL,
DebtAccountLogId STRING(100) NOT NULL, -- DebtLog に対応する DebtAccountLog(DebtAccount の変動ログ)を DebtAccountView に紐付けることで、間接的に DebtLog と紐付けている
) PRIMARY KEY(DebtAccountViewLogId);
上流マイクロサービスは、債権を作成する際に ViewKeyType(集計の軸)と ViewKeyId(ある集計軸における集計ID)を指定すれば、あとはその債権が返済・取消など操作されるたびに自動的に DebtAccountView が更新されます。また軸の種類を増やす際は単に ViewKeyType の種類を増やすだけで十分です。
今後の展望
現状の実装では、一度の債権の操作で更新する DebtAccountView が多くなる、すなわち集計軸が増えていくと、債権操作のレイテンシが上昇してしまいます。そこで考えられる拡張が ViewKeyType ごとに更新の同期・非同期を切り替えられる設計です。初期実装では全 ViewKeyType をデータ書き込みと同じトランザクションで同期的に更新していますが、将来同期的に反映しなければならない与信枠などの軸は同期的に、債権の変動と集計する間にタイムラグがある請求先の軸は非同期で反映するという拡張を予定しています。
また今回事業者請求払いのために DebtAccountView を設計しましたが、toB toC 問わず与信を分割したり与信を付ける先と請求する先が異なる任意のビジネスに適用できます。また、設計の本質は変動ログをピックアップし集計する汎用的なものであるため、債権に限らず残高やポイントを管理する Balance Service に導入して利用することも考えられます。
おわりに
DebtAccountView は、「1つの債権に複数の集計軸を持たせたい」という要件から生まれた機能です。既存の DebtAccount 設計をベースに、「1 DebtLog → N View を更新する」という拡張を汎用的かつシンプルに実現することにこだわりました。また将来のユースケースや拡張も考え抜けたことは、実装を通じて得た達成感の一つです。
事業者請求払いはメルペイの Payment Platform としてまだ進化の途中にあります。与信管理、債権管理、請求、精算という一気通貫のプラットフォームを、各サービスの責務を保ちながら積み上げていくことは、技術的にもドメイン的にも自信を持って面白いと言えます。もしこうした課題に興味を持っていただけたら、ぜひ Balance Team やメルペイの採用・インターン情報も覗いてみてください。この記事が、Fintech や決済基盤のドメイン設計に興味を持つエンジニアにとって何らかのヒントになれば幸いです。
次の記事は yogawaさん と haoyuさん です。引き続きお楽しみください。
Appendix: 採用しなかったその他の選択肢
既存の事業者IDのみでなく与信枠軸や請求先軸など任意の単位での集計を実現する手段として、DebtAccountView 以外にもいくつかの案を検討しました。
1つ目は与信枠ごとに DebtAccount を分割し、請求先の情報は Debt に持たせる方法です。現状のスキーマの延長線上で実装することができとてもシンプルですが、請求先情報という Debt ドメインに関わりが深くない情報を列に直接追加するのはあまりに汎用的でありません。
2つ目は DebtAccount を親子関係で分割する案です。事業者全体の DebtAccount を親として、その子は与信枠の軸で分割、さらにその子は請求先の軸で分割するといったものです。DebtAccount を分割するための軸という概念を追加しそこに与信枠軸や請求先軸を入れるので、1つ目の案よりは汎用的です。しかし与信枠の軸と請求先の軸は互いに独立しているので、与信枠の軸での集計は効率よく行えるものの、請求先の軸での集計は依然効率よく行えません。加えて、与信枠軸や請求先軸のみでなく効率良く集計する必要のある3軸目が登場した場合に、1つ目の案とこの2つ目の案は対応することができません。

3つ目は Debt Service 内でなく、上流サービス側で集計を持つ方法です。Credit Service が与信枠ごとの残債を管理し、Invoice Service が請求先ごとの集計を管理する設計です。しかし Debt Service を使うサービスが増えるたびに、同じ集計ロジックを各サービスが個別に実装することになります。Debt Service 内で集計済みの情報を管理すれば、債権の元データを唯一の情報源として、各サービスへの変更通知なしに信頼性の高い集計を一元的に提供できます。この仕組みを Debt Service に置くことは、将来債権を取り扱う新しいビジネスが高パフォーマンスな集計を必要としたとき、プラットフォームとして即座に応えられる基盤への投資でもあります。



