メルペイ会計チームが訂正機能の設計と実装を公開
メルカリの会計チームが、改変不可な会計データにおける誤り修正のため、元のデータを残して逆仕訳を追加する機能の設計と実装方法を詳述している。
キーポイント
会計データの不変性と訂正手法
会計データは証拠としての役割を持つため改変・削除が許されず、誤りがあった場合は元のデータを残した上で「逆仕訳」を追加して打ち消す手法を採用している。
DB スキーマによる関係性の実装
会計イベントテーブルに元の取引 ID を参照するカラムを追加し、DB 側の一意性制約とアプリケーション層のバリデーションを組み合わせることで整合性を担保している。
レポート集計効率化のためのフラグ設計
逆仕訳判定のために JOIN を行わずに済むよう、仕訳テーブルに独立したフラグ(IsReversal)を持たせ、集計処理の高速化と複雑さの低減を図っている。
手動運用からの自動化への転換
従来は工数とミスリスクを抱えていた手動対応をシステム機能として実装し、構造的な課題を解決して運用コストを削減した。
逆仕訳でのルール検索ロジック
ItemKey が反転した逆仕訳イベントでは、まずキーを元の向きに戻してから仕訳ルールを検索する必要があります。
借方・貸方の属性入れ替え処理
ルールから取得した借方と貸方の各属性(科目コードや金額など)は、逆仕訳の場合に限りコード側で強制的に入れ替えて処理します。
逆仕訳の簡易実装とレポート対応
借方・貸方の入れ替えだけで逆仕訳を作成可能だが、レポート集計では符号反転や条件拡張など個別実装への修正が必要となった。
影響分析・編集コメントを表示
影響分析
本記事は、金融・決済領域における堅牢なデータ設計のベストプラクティスを示しており、特に監査証跡(Audit Trail)とデータ整合性が求められるシステム開発において重要な指針となる。手動運用から自動化への転換プロセスは、大規模取引を扱うプラットフォームがスケーラビリティと信頼性を両立させるための具体的な事例として参考価値が高い。
編集コメント
AI 技術そのものの進展ではないが、大規模取引を扱う企業における堅牢なシステムアーキテクチャの重要性を示す優れた実装事例です。特に監査対応とデータ不変性を両立させる設計思想は、FinTech や基幹系システムの構築において広く応用可能です。
はじめに
こんにちは。メルペイの Accounting チームで Backend Engineer をしている @hokao です。
この記事は、Merpay & Mercoin Tech Openness Month 2026 の 5 日目の記事です。
会計データに誤りがあった場合、元のデータを残したまま打ち消すための記録を別途追加するのが会計上の一般的な手法です。本稿では、これをシステムとしてどう扱ったかを設計と実装の観点から紹介します。
背景
Accounting チームでは、会計データを扱うシステムを開発しています。メルカリグループ全体で発生するお金の移動を伴う取引を記録・集計するシステムで、会計イベントの保存と経理向けのレポーティングを責務としています。
会計データは、取引の事実を証明する証拠としての役割を持ちます。そのため、一度記録したデータを後から改変・削除することは原則として許されません。誤りがあったとしても元データを残したまま、打ち消すための記録を別途追加することで修正します。
しかし、私たちのシステムにはこの打ち消すための機能が存在していませんでした。誤って登録された会計データが見つかるたびに、その件数・金額などの情報を手作業で特定し、経理に連携して対応してもらう必要がありました。この運用には、対応コストの大きさや作業ミスのリスクといった構造的な課題がありました。
以降では、会計ドメインの前提を整理した上で、この課題を解決するために導入した打ち消し機能の設計と実装を順に説明します。
会計ドメインの前提
会計では、すべての取引を借方(デビット)と貸方(クレジット)の 2 つに分けて記録する複式簿記という方式が使われています。借方と貸方それぞれに勘定科目・日付・金額を記載したものが「仕訳」で、これが会計データの最小単位になります。
仕訳に誤りがあった場合、元の仕訳の借方と貸方を入れ替えた「逆仕訳」を計上して打ち消します。
簡単な例として、ある仕入取引を次のように記録していたとします。
- 借方: 仕入 100 円
- 貸方: 現金 100 円
この記録が誤りだった場合、逆仕訳は次のようになります。
- 借方: 現金 100 円
- 貸方: 仕入 100 円
元の仕訳と逆仕訳を合算すると、勘定科目ごとに借方と貸方が打ち消し合い、金額がゼロになります。元データは残したまま、後から追加した記録によって取引を実質的に打ち消す形になります。
ここで説明したのは、逆仕訳がなぜ打ち消しとして成立するのかという会計上の考え方です。実際の会計レポートでは、必ずしも借方と貸方を足し合わせて相殺しているわけではなく、レポートによっては逆仕訳の金額を符号反転させて打ち消しを表現しています。詳しくは後述します。
逆仕訳の設計と実装
スキーマでの逆仕訳の表現
私たちの会計システムでは、上流のマイクロサービスから受け取った取引はまず会計イベントとして記録され、そこから仕訳が作成される構成になっています。それぞれ AccountingEvents と JournalEntries というテーブルで管理されています。

会計イベント (AccountingEvents) には、上流のマイクロサービスから会計の入力として受け取った取引が記録され、会計処理の種類や取引の中身を保持します。これに対して仕訳 (JournalEntries) は、会計イベントに仕訳ルールを適用して必要な属性が確定した、正式な会計記録です。1 つの会計イベントから、仕訳ルールの適用によって複数の仕訳が作成されることもあります。
逆仕訳の会計イベントには、必ず打ち消し対象となる元の会計イベントが存在します。そのため、AccountingEvents に元の会計イベントへの参照 (OriginalTransactionId) を持たせて関係性を表現します。この参照を持つイベントから作成される仕訳はすべて逆仕訳になります。
また、登録時にはこの参照を辿って、次のようなバリデーションを行います。
- 元の会計イベントが存在するか
- 元の会計イベントと整合する勘定科目か
- 元の会計イベントが既に他の逆仕訳によって打ち消されていないか
会計レポートには JournalEntries から集計するものと、分析用に AccountingEvents から集計するものがあります。どちらの場合でも JOIN なしで逆仕訳の判定ができるよう、JournalEntries には、その仕訳が逆仕訳かどうかを表すフラグ (IsReversal) を持たせています。このフラグは本質的には AccountingEvents の OriginalTransactionId から決まる情報ですが、JournalEntries でも独立に判定できるよう別途持たせています。
同じ会計イベントに対する逆仕訳の会計イベントが複数存在してはいけないため、上述のアプリケーション側のバリデーションに加えて、DB 側にも一意性制約を設けています。具体的には、OriginalTransactionId カラムに NULL_FILTERED オプションを指定した UNIQUE INDEX を張っています。これにより、OriginalTransactionId が NULL のイベント (通常の会計イベント) は重複扱いされず、NULL でない値だけに一意性制約がかかります。
これらをまとめると、スキーマの該当箇所は次のようになります。
CREATE TABLE AccountingEvents (
EventId STRING(100) NOT NULL,
AccountingCode STRING(MAX) NOT NULL,
OriginalTransactionId STRING(100),
-- ...
) PRIMARY KEY (EventId);
CREATE NULL_FILTERED INDEX AccountingEventsByOriginalTransactionId
ON AccountingEvents(OriginalTransactionId);
CREATE TABLE JournalEntries (
Id STRING(100) NOT NULL,
EventId STRING(100) NOT NULL,
IsReversal BOOL NOT NULL,
-- ...
) PRIMARY KEY (Id);
仕訳作成における借方と貸方の入れ替え
逆仕訳(リバーサルエントリ)の作成は、元の取引と同じ仕訳ルールを再利用しつつ、そのルールから抽出された借方(デビット)と貸方(クレジット)の属性をコード側で入れ替えることで行われます。
会計イベントには、取引の方向を表す「ItemKey」という識別子が含まれており、これは X.to.Y という形式を取ります。仕訳ルールもこの ItemKey をキーとして登録されています。逆仕訳イベントでは ItemKey が反転して Y.to.X の形で届くため、ルールを検索する際は ItemKey を一度元の向きに戻す処理が必要です。
ルールを取得した後、逆仕訳イベントの場合に限り、ルールから取り出した借方と貸方の各属性をコード側で入れ替えます。簡略化した擬似コードでは次のようになります。
originalRule := getOriginalEventRule(ev)
debitAccountingTitleCode := originalRule.DebitAccountingTitleCode
creditAccountingTitleCode := originalRule.CreditAccountingTitleCode
debitXxx := originalRule.DebitXxx
creditXxx := originalRule.CreditXxx
// ...
if isReversal {
debitAccountingTitleCode, creditAccountingTitleCode = creditAccountingTitleCode, debitAccountingTitleCode
debitXxx, creditXxx = creditXxx, debitXxx
// ...
}
debit := JournalEntry{
AccountingTitleCode: debitAccountingTitleCode,
Xxx: debitXxx,
// ...
IsReversal: isReversal,
}
credit := JournalEntry{
AccountingTitleCode: creditAccountingTitleCode,
Xxx: creditXxx,
// ...
IsReversal: isReversal,
}
仕訳ルールが借方と貸方の対称な構造を持っているため、ItemKey の反転とそれに続く借方と貸方の入れ替えという 2 つの操作だけで逆仕訳を作成でき、実装はシンプルな修正で済みました。
会計レポートにおける逆仕訳の打ち消し処理
会計レポートに打ち消しを反映させるには、集計クエリ(アグリゲーションクエリ)において逆仕訳または逆仕訳に関連する会計イベントを判定し、金額を正しく計算する必要があります。
JournalEntries から集計するクエリでは IsReversal フラグで判定し、分析用に AccountingEvents から集計するクエリでは OriginalTransactionId IS NOT NULL で判定します。
会計ドメインの前提で述べたとおり、本来の逆仕訳は、勘定科目ごとに借方と貸方を足し合わせて相殺することで打ち消しを実現します。ただし、会計レポートには借方または貸方のどちらか一方だけを集計するものもあり、そうしたレポートでは足し合わせによる相殺は成立しません。そのため、集計クエリで逆仕訳または逆仕訳の会計イベントの金額を符号反転して合算するアプローチをとっています。なお、逆仕訳の金額自体は元の仕訳と同じ正の値で保存しています。
会計レポートは、対象とするテーブルや集計の意味合いがそれぞれ異なるため、共通化が難しく、もともと個別に実装されています。逆仕訳の打ち消しを反映するためには、その個別実装それぞれに修正を入れる必要がありました。例えば WHERE 句の絞り込み条件を、逆仕訳の会計イベントも拾えるように拡張するなどです。すべてのレポートに対してこのような修正を加えていったため、実装と検証には時間がかかりました。
逆仕訳バッチによる運用の自動化
加えて、誤って登録された会計データを訂正するために、逆仕訳を作成するためのバッチを新たに実装しました。このバッチは、対象となる取引の ID リストを受け取り、それぞれについて、上流のマイクロサービスの API を介して打ち消し用の取引を作成します。それが会計イベントとして本システムに登録され、逆仕訳が自動的に作成されます。
個別の取引で失敗が発生した場合はログに記録しつつ、残りの処理は継続します。また、冪等性は上流のマイクロサービス側で保証されているため、同じ ID リストで再実行することも可能です。
これにより、誤ったデータの打ち消しをシステム上で自動的に逆仕訳として反映できるようになり、これまで手作業で行っていた特定・連携の負荷と作業ミスのリスクが大きく軽減されました。
おわりに
本稿では、会計データの訂正を支える逆仕訳機能の設計と実装を紹介しました。
会計のように不変性の制約が強いドメインで開発していると、ドメインの原則が設計判断をそのまま導いてくれる場面が多く、その面白さを今回の機能開発でも改めて感じました。
今回の機能開発は、Accountingチームが抱える運用負荷削減という大きな取り組みの一環でもあります。会計システムは、会社が財務状況を正しく把握するための基盤であり、事業の成長に合わせてスケールできる状態に保ち続ける必要があります。これからも、持続的な開発ができるよう取り組んでいきたいと考えています。
次の記事は imamu さんです。引き続きお楽しみください。
原文を表示
はじめに
こんにちは。メルペイのAccountingチームでBackend Engineerをしている@hokaoです。
この記事は、Merpay & Mercoin Tech Openness Month 2026 の 5 日目の記事です。
会計データに誤りがあった場合、元のデータを残したまま打ち消すための記録を別途追加するのが会計上の一般的な手法です。本稿では、これをシステムとしてどう扱ったかを設計と実装の観点から紹介します。
背景
Accountingチームでは、会計データを扱うシステムを開発しています。メルカリグループ全体で発生するお金の移動を伴う取引を記録・集計するシステムで、会計イベントの保存と経理向けのレポーティングを責務としています。
会計データは、取引の事実を証明する証拠としての役割を持ちます。そのため、一度記録したデータを後から改変・削除することは原則として許されません。誤りがあったとしても元データを残したまま、打ち消すための記録を別途追加することで修正します。
しかし、私たちのシステムにはこの打ち消すための機能が存在していませんでした。誤って登録された会計データが見つかるたびに、その件数・金額などの情報を手作業で特定し、経理に連携して対応してもらう必要がありました。この運用には、対応コストの大きさや作業ミスのリスクといった構造的な課題がありました。
以降では、会計ドメインの前提を整理した上で、この課題を解決するために導入した打ち消し機能の設計と実装を順に説明します。
会計ドメインの前提
会計では、すべての取引を借方と貸方の 2 つに分けて記録する複式簿記という方式が使われています。借方と貸方それぞれに勘定科目・日付・金額を記載したものが「仕訳」で、これが会計データの最小単位になります。
仕訳に誤りがあった場合、元の仕訳の借方と貸方を入れ替えた「逆仕訳」を計上して打ち消します。
簡単な例として、ある仕入取引を次のように記録していたとします。
- 借方: 仕入 100 円
- 貸方: 現金 100 円
この記録が誤りだった場合、逆仕訳は次のようになります。
- 借方: 現金 100 円
- 貸方: 仕入 100 円
元の仕訳と逆仕訳を合算すると、勘定科目ごとに借方と貸方が打ち消し合い、金額がゼロになります。元データは残したまま、後から追加した記録によって取引を実質的に打ち消す形になります。
ここで説明したのは、逆仕訳がなぜ打ち消しとして成立するのかという会計上の考え方です。実際の会計レポートでは、必ずしも借方と貸方を足し合わせて相殺しているわけではなく、レポートによっては逆仕訳の金額を符号反転させて打ち消しを表現しています。詳しくは後述します。
逆仕訳の設計と実装
スキーマでの逆仕訳の表現
私たちの会計システムでは、上流のマイクロサービスから受け取った取引はまず会計イベントとして記録され、そこから仕訳が作成される構成になっています。それぞれ AccountingEvents と JournalEntries というテーブルで管理されています。

会計イベント (AccountingEvents) には、上流のマイクロサービスから会計の入力として受け取った取引が記録され、会計処理の種類や取引の中身を保持します。これに対して仕訳 (JournalEntries) は、会計イベントに仕訳ルールを適用して必要な属性が確定した、正式な会計記録です。1 つの会計イベントから、仕訳ルールの適用によって複数の仕訳が作成されることもあります。
逆仕訳の会計イベントには、必ず打ち消し対象となる元の会計イベントが存在します。そのため、AccountingEvents に元の会計イベントへの参照 (OriginalTransactionId) を持たせて関係性を表現します。この参照を持つイベントから作成される仕訳はすべて逆仕訳になります。
また、登録時にはこの参照を辿って、次のようなバリデーションを行います。
- 元の会計イベントが存在するか
- 元の会計イベントと整合する勘定科目か
- 元の会計イベントが既に他の逆仕訳によって打ち消されていないか
会計レポートには JournalEntries から集計するものと、分析用に AccountingEvents から集計するものがあります。どちらの場合でも JOIN なしで逆仕訳の判定ができるよう、JournalEntries には、その仕訳が逆仕訳かどうかを表すフラグ (IsReversal) を持たせています。このフラグは本質的には AccountingEvents の OriginalTransactionId から決まる情報ですが、JournalEntries でも独立に判定できるよう別途持たせています。
同じ会計イベントに対する逆仕訳の会計イベントが複数存在してはいけないため、上述のアプリケーション側のバリデーションに加えて、DB 側にも一意性制約を設けています。具体的には、OriginalTransactionId カラムに NULL_FILTERED オプションを指定した UNIQUE INDEX を張っています。これにより、OriginalTransactionId が NULL のイベント (通常の会計イベント) は重複扱いされず、NULL でない値だけに一意性制約がかかります。
これらをまとめると、スキーマの該当箇所は次のようになります。
CREATE TABLE AccountingEvents (
EventId STRING(100) NOT NULL,
AccountingCode STRING(MAX) NOT NULL,
OriginalTransactionId STRING(100),
-- ...
) PRIMARY KEY (EventId);
CREATE NULL_FILTERED INDEX AccountingEventsByOriginalTransactionId
ON AccountingEvents(OriginalTransactionId);
CREATE TABLE JournalEntries (
Id STRING(100) NOT NULL,
EventId STRING(100) NOT NULL,
IsReversal BOOL NOT NULL,
-- ...
) PRIMARY KEY (Id);仕訳作成での借方/貸方の入れ替え
逆仕訳の仕訳作成は、元の取引と同じ仕訳ルールを再利用しつつ、ルールから取り出した借方と貸方の属性をコード側で入れ替えて行います。
会計イベントには取引の方向を表す ItemKey という識別子が含まれ、X.to.Y の形式を取ります。仕訳ルールもこの ItemKey をキーに登録されています。逆仕訳イベントでは ItemKey が反転して Y.to.X の形で届くため、ルールを検索する際は ItemKey を一度元の向きに戻します。
ルールを取得した後、逆仕訳イベントの場合に限り、ルールから取り出した借方と貸方の各属性をコード側で入れ替えます。簡略化した擬似コードで示すと次のようになります。
originalRule := getOriginalEventRule(ev)
debitAccountingTitleCode := originalRule.DebitAccountingTitleCode
creditAccountingTitleCode := originalRule.CreditAccountingTitleCode
debitXxx := originalRule.DebitXxx
creditXxx := originalRule.CreditXxx
// ...
if isReversal {
debitAccountingTitleCode, creditAccountingTitleCode = creditAccountingTitleCode, debitAccountingTitleCode
debitXxx, creditXxx = creditXxx, debitXxx
// ...
}
debit := JournalEntry{
AccountingTitleCode: debitAccountingTitleCode,
Xxx: debitXxx,
// ...
IsReversal: isReversal,
}
credit := JournalEntry{
AccountingTitleCode: creditAccountingTitleCode,
Xxx: creditXxx,
// ...
IsReversal: isReversal,
}仕訳ルールが借方と貸方の対称な構造を持っているため、ItemKey の反転とそれに続く借方と貸方の入れ替えという 2 つの操作だけで逆仕訳を作成でき、実装はシンプルな修正で済みました。
会計レポートでの逆仕訳の打ち消し
会計レポートに打ち消しを反映するには、集計クエリで逆仕訳または逆仕訳の会計イベントを判定し、正しく金額を計算する必要があります。
JournalEntries から集計するクエリでは IsReversal フラグで判定し、分析用に AccountingEvents から集計するクエリでは OriginalTransactionId IS NOT NULL で判定します。
会計ドメインの前提で述べたとおり、本来の逆仕訳は、勘定科目ごとに借方と貸方を足し合わせて相殺することで打ち消しを実現します。ただし、会計レポートには借方または貸方のどちらか一方だけを集計するものもあり、そうしたレポートでは足し合わせによる相殺は成立しません。そのため、集計クエリで逆仕訳または逆仕訳の会計イベントの金額を符号反転して合算するアプローチをとっています。なお、逆仕訳の金額自体は元の仕訳と同じ正の値で保存しています。
会計レポートは、対象とするテーブルや集計の意味合いがそれぞれ異なるため、共通化が難しく、もともと個別に実装されています。逆仕訳の打ち消しを反映するためには、その個別実装それぞれに修正を入れる必要がありました。例えば WHERE 句の絞り込み条件を、逆仕訳の会計イベントも拾えるように拡張するなどです。すべてのレポートに対してこのような修正を加えていったため、実装と検証には時間がかかりました。
逆仕訳バッチによる運用の自動化
加えて、誤って登録された会計データを訂正するために、逆仕訳を作成するためのバッチを新たに実装しました。このバッチは、対象となる取引の ID リストを受け取り、それぞれについて、上流のマイクロサービスの API を介して打ち消し用の取引を作成します。それが会計イベントとして本システムに登録され、逆仕訳が自動的に作成されます。
個別の取引で失敗が発生した場合はログに記録しつつ、残りの処理は継続します。また、冪等性は上流のマイクロサービス側で保証されているため、同じ ID リストで再実行することも可能です。
これにより、誤ったデータの打ち消しをシステム上で自動的に逆仕訳として反映できるようになり、これまで手作業で行っていた特定・連携の負荷と作業ミスのリスクが大きく軽減されました。
おわりに
本稿では、会計データの訂正を支える逆仕訳機能の設計と実装を紹介しました。
会計のように不変性の制約が強いドメインで開発していると、ドメインの原則が設計判断をそのまま導いてくれる場面が多く、その面白さを今回の機能開発でも改めて感じました。
今回の機能開発は、Accountingチームが抱える運用負荷削減という大きな取り組みの一環でもあります。会計システムは、会社が財務状況を正しく把握するための基盤であり、事業の成長に合わせてスケールできる状態に保ち続ける必要があります。これからも、持続的な開発ができるよう取り組んでいきたいと考えています。
次の記事は imamu さんです。引き続きお楽しみください。
関連記事
決済プラットフォームと経理を繋ぐ MoneyFlow の紹介
メルカリの Payment Core チームおよび Payment Solution チームが、決済と会計の間で共通言語として機能する「MoneyFlow」を開発し、Merpay & Mercoin Tech Openness Month 2026 で発表した。
NoSQL(mongoDB)導入ガイド
RDBに慣れたエンジニア向けに、NoSQLの利点と導入方法を解説。多様化するロールに対応し、適切な場面での利用を促進するためのガイド。
AI と作る LP エディタ EGP Code を支える 4 つの仕組み
メルペイのフロントエンドエンジニアが、ランディングページを AI で作成する社内ツール「EGP Code」を支える 4 つの仕組みについて解説している。
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み