複式簿記とソフトウェア設計
確定申告のために複式簿記で記帳していると、これは会計固有のルールというより、状態管理の設計パターンに近いと感じる。単式簿記を複式簿記へ変えたとき、増えるのは記帳の手間だけではない。記録される情報の量と構造が変わる。
この記事では、単式簿記では失われてしまう情報が複式簿記では何として残るのかを整理し、それがソフトウェア設計のどの考え方に対応するのかをまとめる。
まとめ
- 単式簿記は残高を上書きする設計、複式簿記は取引を追記する設計だと考えられる
- 単式簿記で失われるのは相手勘定、つまり「金額が動いた原因」と「その裏側で何が増減したか」という対の情報
- 仕訳は immutable な record として扱う。誤りは書き換えではなく逆仕訳で打ち消す。これは event sourcing とほぼ同じ構造になっている
- 残高は仕訳を畳み込んだ導出値であり、決算や月次の締めは snapshot に相当する
- 借方合計と貸方合計が一致することは常に成り立つ不変条件で、二重記入という冗長性がエラー検出を支えている
- 元入金や事業主貸借は、事業という閉じた世界と外界をつなぐ限られた出入口になっている。外部との入出力を境界に集約する設計と発想が近い
- 会計をそのまま模倣するのではなく、「追記のみ」「保存則」「境界の明示」の3点を借りると、堅牢なアプリケーション設計に寄せられる
前提
- 会計用語は日本の個人事業主の簿記を前提にする。法人であれば元入金は資本金、事業主貸借は役員貸付金や配当などに読み替える
- 掲載するコードは Go 1.27 系で
go testを通したものを使う - 税務や制度の扱いは改正で変わる場合がある。実務では公式情報や税理士に確認する
- 想定読者は、簿記の細部よりも設計の考え方に興味がある開発者とする
単式簿記と複式簿記で残る情報の差
単式簿記は残高の履歴しか持たない
単式簿記は、現金や預金といった1つの側面だけを追う。家計簿がわかりやすい例で、次のように書く。
| 日付 | 摘要 | 入金 | 出金 | 残高 |
|---|---|---|---|---|
| 4/1 | 元手を入れる | 300,000 | 300,000 | |
| 4/10 | 備品を買う | 20,000 | 280,000 | |
| 4/30 | 入金あり | 100,000 | 380,000 |
これでも現金残高は正しく求まる。しかし、4/30 の入金が何なのかはわからない。今月の売上かもしれないし、先月の売掛金の回収かもしれないし、借入かもしれない。摘要欄の自由記述に頼るしかなく、機械的には区別できない。
複式簿記は取引の両側を書く
複式簿記では、1つの取引を必ず借方と貸方の2面で書く。同じ出来事は次のようになる。
| 日付 | 借方 | 金額 | 貸方 | 金額 |
|---|---|---|---|---|
| 4/1 | 現金 | 300,000 | 元入金 | 300,000 |
| 4/5 | 売掛金 | 100,000 | 売上 | 100,000 |
| 4/10 | 消耗品費 | 20,000 | 現金 | 20,000 |
| 4/30 | 現金 | 100,000 | 売掛金 | 100,000 |
4/30 の入金は「売掛金の回収」であり、売上ではないことが記録から読み取れる。売上そのものは 4/5 に発生していて、現金の動きとは別の日付を持つ。
単式簿記で失われる情報
差分を並べると、失われているのは次の情報になる。
- 相手勘定: 現金が増えた原因は何か。売上か、回収か、借入か、元手か
- 因果の対: 資産が増えるとき、同時に何が減った、あるいは何が増えたのか
- 現金以外の残高: 売掛金、買掛金、借入金といった、まだ現金化していない権利や義務
- 発生の時点: 売上が立った日と、入金された日の区別
- 損益と財産の分離: 手元の現金が増えたのは儲かったからか、借りたからかの区別
- 検算の材料: 二重に書いていないため、記入漏れを内部矛盾として検出できない
まとめると、単式簿記は「残高」というスカラー値の履歴しか持たないのに対して、複式簿記は「どの勘定からどの勘定へ、いつ、いくら動いたか」という構造を持つ。ここからソフトウェアの話に接続できる。
flowchart LR
subgraph S[単式簿記]
s1[取引] --> s2[現金残高を更新]
end
subgraph D[複式簿記]
d1[取引] --> d2[仕訳を追記]
d2 --> d3[借方: 何が増えたか]
d2 --> d4[貸方: 何が減ったか]
d3 --> d5[残高は畳み込みで導出]
d4 --> d5
end特徴1: 仕訳は immutable な record である
訂正は書き換えではなく逆仕訳で行う
簿記では、記帳済みの仕訳を消したり書き換えたりしない。誤りが見つかったときは、元の仕訳を打ち消す逆仕訳、いわゆる赤伝を追加してから、正しい仕訳を入れ直す。帳簿には「間違えた」「取り消した」「入れ直した」の3件が残る。
これは、税務調査や監査で履歴の改竄を防ぐための実務上の要請でもある。しかし、設計として見ても筋が通っている。
- 過去の残高を後から再現できる
- 誰がいつ何を訂正したかが追える
- 訂正そのものが分析対象になる。どの科目で誤りが多いかが見える
event sourcing との対応
このやり方は、ソフトウェアでいう append-only log や event sourcing とほぼ同じ構造になっている。
| 簿記 | ソフトウェア |
|---|---|
| 仕訳 | イベント、コミット、ログレコード |
| 仕訳帳 | イベントストア、追記専用ログ |
| 逆仕訳(赤伝) | 補償イベント、revert コミット |
| 総勘定元帳、残高 | projection、read model |
| 月次や年次の締め | snapshot、チェックポイント |
| 試算表 | 整合性チェック、reconciliation |
同じ発想は身近な実装にも多い。Git は commit を書き換えず、取り消しには git revert で打ち消しコミットを積める。データベースの WAL は変更を追記してから状態に反映する。Kafka のログも追記のみで、消費側が自分の位置を持つ。
上書き更新で失われるもの
行を UPDATE で書き換える設計は、単式簿記と同じ問題を抱える。
- なぜその値になったのか、原因が残らない
- 障害調査のときに、途中経過を再現できない
- 集計の定義を変えたときに、過去に遡って計算し直せない
- 同時更新が衝突したとき、どちらの意図が失われたのかを判断できない
「あとから聞かれそうな問いに答えられるか」を基準にすると、追記のほうが安全な場面は多い。特に、金額、在庫、権限、契約状態のように、後から説明責任が発生する領域では効いてくる。
特徴2: 残高は導出値であり、締めは snapshot である
複式簿記では、残高は帳簿に書いてある「事実」ではなく、仕訳の集合から計算される導出値になる。
| |
つまり、残高は仕訳列の畳み込み(fold)で表せる。ここから2つの性質が出てくる。
- 任意の時点の残高を再現できる。過去の帳簿を「その日の状態」として復元できる
- 集計軸をあとから増やせる。部門別、プロジェクト別、税区分別といった見方を、元帳を作り直さずに追加できる
一方で、毎回すべての仕訳を畳み込むのは効率が悪い。そこで簿記では、期末に決算して繰越残高を確定させ、翌期は開始残高から積み上げる。これは、event sourcing で長いイベント列を毎回再生しないために snapshot を取るのと同じ発想で、次のように整理できる。
- snapshot は導出値なので、壊れても元のログから作り直せる
- ただし、締めた期間のイベントを後から追加すると snapshot と食い違う。簿記でも、締めた期に遡る修正は特別扱いになる
- そのため「どこまで確定したか」を明示する境界が必要になる
特徴3: 貸借一致という不変条件
常に成り立つ等式
複式簿記には、常に成り立つ不変条件がある。
借方を正、貸方を負として符号付きで持てば、これは「全明細の合計が常に 0」と表現できる。試算表で借方合計と貸方合計を突き合わせるのは、この不変条件が破れていないかを確認する作業になる。
ここで重要なのは、二重記入が冗長性を生んでいる点だ。同じ取引を2箇所に書くので、片方を書き忘れると合計が合わなくなる。誤りが「静かに間違った値」ではなく「検出可能な矛盾」として現れる。パリティビットやチェックサムと同じ考え方といえる。
Go で最小の台帳を書く
不変条件は、コメントに書くのではなく、型と関数で守るほうが確実になる。仕訳を作る唯一の経路で検証すれば、不正な仕訳はそもそも存在できない。
| |
Go には immutable を宣言する構文がない。const が使えるのは基本型の定数だけなので、フィールドを公開せず、コンストラクタを唯一の入口にして近似する。可変長引数で受け取ったスライスを slices.Clone でコピーしている点も重要で、これを省くと呼び出し側と backing array を共有するため、検証を通した後から金額を書き換えられる。外部パッケージへ渡す場合も、内部のスライスをそのまま返す getter は作らない。
テストでは、個々の計算結果に加えて、不変条件そのものを検証しておくとよい。全勘定の残高合計が 0 になることは、どんな仕訳列に対しても成り立つ性質になる。
| |
実行結果は次のようになる。
データベース側で追記のみに寄せる
永続化層でも、上書きを構造的に禁止しておくと安心できる。
| |
注意点として、「1仕訳の中で合計が 0」という条件は、行単位の CHECK 制約では書けない。実現するには、次のような選択肢がある。
- 書き込み経路をアプリケーション側の1関数に絞り、そこで検証してからトランザクションで挿入する
- 仕訳単位のトリガや、遅延評価される制約(
DEFERRABLEな制約)で検査する - 仕訳ヘッダに合計額を持たせ、明細の合計と突き合わせるバッチを定期実行する
どれを選んでも、「壊れた状態を作れない経路」と「壊れていないことを後から確認する経路」の両方を持たせるのが安全になる。
特徴4: 元入金は外界との境界を管理する仕組み
事業と個人を分ける
個人事業主の簿記では、事業と個人の財布を分けて考える。事業を始めるときに個人の資金を投入すると、元入金として記録する。法人でいう資本金にあたる。
| |
事業を続けている間に、個人の口座から事業の支払いをしたり、逆に事業の現金を生活費に回したりすることもある。この場合は事業主借、事業主貸という勘定を使う。
重要なのは、事業という閉じた世界の外と資金がやりとりされるとき、必ず専用の勘定を通ることだ。外界との出入りは、どこか適当な場所で起きるのではなく、決められた口を通る。この前提があるおかげで、次の性質が保証される。
- 事業内部の取引だけでは、純資産の総量は変わらない。現金で備品を買っても、資産の内訳が入れ替わるだけになる
- 純資産が動くのは、損益が出たときか、外界との資本取引があったときに限られる
これは物理でいう保存則に近い。系の内部で何が起きても総量は保存され、境界を越える出入りだけが総量を変える。
flowchart LR
outside([外界: 個人の財布・出資者・金融機関])
subgraph biz[事業という閉じた系]
capital[元入金・事業主貸借]
assets[資産・負債]
pl[収益・費用]
capital --> assets
assets <--> pl
end
outside -- 資金の投入 --> capital
capital -- 引き出し --> outsideソフトウェア設計での対応
同じ構造は、堅牢なアプリケーション設計にそのまま現れる。
- 境界を1箇所に集める: 外部 API、決済、ファイル、時刻、乱数といった外界に触れる処理を、境界の層に閉じ込める。ports and adapters や、functional core / imperative shell と呼ばれる考え方に近い
- 内部は閉じた不変条件を守る: 境界の内側は純粋な計算にして、不変条件をテストで検証できる状態にする
- 出入口を数える: 外界と接続する経路が限られていれば、監査やモックへの差し替えが容易になる。逆に、どこからでも外部呼び出しが起きるコードは、状態の由来を追えない
- 入ってくるデータは境界で検証する: 元入金の記帳が事業の外から来た資金に名前を与える作業だとすれば、入力バリデーションは外部データに型を与える作業になる
金額を扱うシステムでは、この対応は比喩以上の意味を持つ。たとえば決済代行サービスと連携するとき、外部から入ってくる入金は必ず「外部システム」勘定を経由させると、次の点が扱いやすくなる。
- 自社の帳簿と外部の明細を突き合わせる作業(reconciliation)が、勘定残高の比較になる
- webhook の重複配信は、同じ取引 ID の仕訳が既にあるかどうかで冪等に処理できる
- 未達の入金は、突き合わせで残高が残る形で表面化する
特徴5: 勘定科目は型であり集計軸である
勘定科目は、資産、負債、純資産、収益、費用の5つに分類される。この分類が決まると、借方と貸方のどちらが増加を意味するかも決まる。
| 分類 | 増加する側 | 例 | ソフトウェアでの見方 |
|---|---|---|---|
| 資産 | 借方 | 現金、売掛金、備品 | 保有しているリソース |
| 負債 | 貸方 | 買掛金、借入金 | 将来引き渡す義務 |
| 純資産 | 貸方 | 元入金、資本金 | 外界から持ち込まれた元手 |
| 収益 | 貸方 | 売上、雑収入 | 期間中に増えた価値のフロー |
| 費用 | 借方 | 消耗品費、支払手数料 | 期間中に消えた価値のフロー |
科目名を自由記述ではなく決まった集合から選ぶ点は、文字列ではなく列挙型や値オブジェクトで表すことに対応する。実装でも Account を string の別名にとどめず、登録済みの科目だけを許す構造にしておくと、集計漏れや表記ゆれを防げる。
さらに、資産や負債は「ある時点の状態(stock)」、収益や費用は「期間の変化(flow)」を表す。この2つを混ぜないという規律は、状態と差分を分けて設計する感覚に近い。貸借対照表は状態のスナップショット、損益計算書は期間の集計であり、両者は当期純利益で接続する。
特徴6: 発生主義は未確定の状態を明示する
現金主義では、現金が動いたときだけ記録する。発生主義では、取引が発生した時点で記録し、入出金は別の事象として扱う。この差が売掛金や買掛金という勘定を生む。
売掛金は「まだ受け取っていないが、受け取る権利がある」状態を表す。ソフトウェア設計では、次のような状態に対応する。
- 決済の与信確保だけを済ませ、確定は後で行う二段階の処理
- 分散トランザクションにおける未確定の状態や、補償処理を前提にした saga
- 予約済みだが未消費の在庫
ここでの教訓は、「中間状態に名前を付けて残高として持つ」ことにある。中間状態を暗黙にすると、処理が途中で落ちたときに、どこまで進んだのかがわからなくなる。売掛金という残高があれば、回収されていない取引は残高として見える。滞留している中間状態は、集計するだけで検出できる。
対応表としてのまとめ
| 複式簿記の性質 | ソフトウェア設計での対応 | 何が守られるか |
|---|---|---|
| 仕訳を追記し書き換えない | append-only log、event sourcing | 履歴の追跡、再現性 |
| 誤りは逆仕訳で打ち消す | 補償イベント、revert | 監査可能性 |
| 残高は仕訳の畳み込み | projection、read model | 集計軸の追加、再計算 |
| 決算や月次の締め | snapshot、チェックポイント | 性能と確定範囲の明示 |
| 借方合計 = 貸方合計 | 不変条件、型と検証、テスト | 壊れた状態を作らせない |
| 二重記入の冗長性 | チェックサム、突き合わせ | 誤りの検出 |
| 相手勘定を必ず書く | 原因と結果を対で記録する | 変更理由の追跡 |
| 元入金と事業主貸借 | 外部 I/O の境界集約、入力検証 | 外界の影響範囲の限定 |
| 勘定科目の5分類 | 型、値オブジェクト、集計軸 | 表記ゆれと集計漏れの防止 |
| 発生主義の売掛金や買掛金 | 中間状態の明示、saga、予約 | 未完了処理の可視化 |
注意点
- 会計をそのまま実装しない: 業務システムに複式簿記を導入すると、開発と運用のコストは確実に増える。借りるべきは「追記のみ」「保存則」「境界の明示」といった考え方であり、勘定科目体系まで持ち込むかは要件次第になる
- 金額に浮動小数点数を使わない: 金額は最小通貨単位の整数か、10進の固定小数点型で持つ。
float64の丸め誤差は貸借一致を静かに壊す - 多通貨は別の話になる: 通貨をまたぐと、通貨ごとに貸借一致を保ちつつ、換算差額を別勘定で受ける設計が必要になる。単純に合計すると一致しない
- immutable と削除要件は衝突する場合がある: 個人情報を含むログでは、削除請求への対応が必要になることがある。金額そのものと個人情報を分離し、参照側だけ削除できる構造にしておくと逃げ道を作りやすい。法令の適用は事案によって異なるため、公式情報や専門家に確認する
- 追記のみはストレージを消費する: 保持期間、アーカイブ、snapshot の運用方針を最初に決めておく。無限に貯め続ける前提には無理がある
- 不変条件は検証してこそ意味がある: 制約を書くだけでなく、定期的な突き合わせジョブや、property based test に近い形の検証を用意する
- 税務上の判断は範囲外: この記事で扱うのは設計の話であり、記帳方法の妥当性や控除の可否には触れていない。制度は改正されるため、実務では最新の公式情報を確認する