業務システムをシンプルに保つ3層構造設計

― 複雑な業務をシンプルに扱う 3 層構造と更新規則 ―

エンジニア向け設計思想ガイド

対象読者:業務システム開発に携わるすべてのエンジニア


0. はじめに ― なぜシステムは修正のたびに壊れるのか

あなたは、こんな経験をしたことはないだろうか。

  • 小さな仕様変更のつもりが、まったく別の画面でバグが出た。
  • 同じような処理なのに、担当者によって書き方がバラバラで、読解に時間がかかる。
  • 「ここを直せばいい」と思ったのに、直す場所が 5 か所もあった。

これらは「スキル不足」ではない。構造の問題だ。

業務システムには、二つの性質がある。

要素 性質
業務ルール 複雑で、頻繁に変わる
コンピュータがやること データの追加・更新・削除という単純な操作の繰り返し

この「複雑な業務」と「シンプルなコンピュータ」をどう繋ぐか。それを整理するのが、本ドキュメントの目的だ。

📌 この文書で伝えること
言語やフレームワークに依存しない、業務システム設計の原則を示す。現代のアーキテクチャで実装する際の指針として活用してほしい。

1. 3 層構造の概要

業務システムの処理は、次の 3 つの責務に分けて設計する。

名称 責務
第 1 層 エントリー層 業務イベントを受け取り、入力検証を行い、正規化されたデータを次層へ渡す
第 2 層 正規化・振分け層 受け取ったデータを各ファイル(テーブル)への更新レコードに変換・振り分ける
第 3 層 更新層 渡されたレコードを、決められたルールだけでデータストアに反映する

1-1. 構造の流れ

ユーザー操作 / 外部イベント

第 1 層:エントリー層
入力検証・イベント種別判定(登録 / 修正 / 削除)・次層への受け渡し
※ 業務の複雑さはここで受け切る / データストアへの直接書き込み禁止

イベントレコード(種別コード付き)

第 2 層:正規化・振分け層
イベントを各テーブルへの更新レコードに変換・振り分ける
※ 業務ロジックはここに集約 / UI への直接アクセス禁止

更新レコード(テーブルごと)

更新層
テーブル A
更新層
テーブル B
更新層
テーブル C
更新層
テーブル D ・・・

※ ルールだけで動く(数値→加算、テキスト→転送、NULL→無視) 業務ロジック・条件分岐禁止

🔑「業務の複雑さ」は第 1・2 層で吸収する。
第 3 層には複雑さを持ち込まない。どのイベントが来ても同じルールで動く。
🔑 この構造の核心
「業務の複雑さ」は第 1 層と第 2 層で吸収する。第 3 層には複雑さを持ち込まない。更新層はどのイベントが来ても同じルールで動く。

1-2. 各層の「やること・やってはいけないこと」

やること やってはいけないこと
第 1 層
エントリー層
入力値の検証、参照データの取得、イベント種別(登録 / 修正 / 削除)の判定、正規化・振分け層への受け渡し データストアへの直接書き込み(採番管理など最小限の例外を除く)
第 2 層
正規化・振分け層
イベントレコードを各テーブルの更新レコードに変換、更新対象テーブルの振り分け、ログの出力。イベントレコードだけでは更新値を確定できない場合は、値の補完を目的としてデータストアを参照することができる(補完参照ルール参照) 画面・UI への直接アクセス、入力検証の再実施
第 3 層
更新層
渡されたレコードを「数値なら加算、テキストなら転送、NULL/未セットなら無視」のルールで更新 業務ロジックの判断、条件分岐、他テーブルの参照

2. イベント種別と制御コード

第 1 層は、ユーザー操作を次の 3 種類のイベントとして扱う。それぞれに「制御コード」を付与して第 2 層へ渡す。

操作 制御コード 渡すレコード数と内容
新規登録 CREATE (1) 1 件:登録内容
修正 DELETE (2) + CREATE (1) 2 件:修正前レコード(DELETE)+修正後レコード(CREATE)
削除 DELETE (2) 1 件:削除対象レコード
💡 なぜ修正を「削除 + 登録」で表現するのか
修正をそのまま渡すと、第 3 層が「どの項目が変わったのか」を判断しなければならなくなる。削除と登録に分解することで、第 3 層は判断ゼロで動ける。これが構造をシンプルに保つ鍵だ。

2-1. 現代の実装との対応

制御コードの考え方は、現代のイベント駆動アーキテクチャと自然に対応する。

本ドキュメントの概念 現代の実装例
制御コード CREATE (1) イベントタイプ: ItemCreated / OrderPlaced
制御コード DELETE (2) イベントタイプ: ItemDeleted / OrderCancelled
修正 = DELETE + CREATE イベントタイプ: ItemUpdated(旧値スナップショット付き)
イベントレコード メッセージキュー(Kafka, SQS)のメッセージ / ドメインイベント
第 2 層(正規化・振分け) イベントハンドラー / サガ(Saga)パターン
第 3 層(更新) リポジトリ層 / O/R マッパーの upsert 処理

3. 更新層の更新規則

第 3 層(更新層)は、「判断しない」ことが最大の特徴だ。渡されたレコードの各フィールドを、次のルールだけで処理する。

フィールドの状態 処理 補足
数値がセットされている 現在値に加算する DELETE コードの場合は負の値を渡すことで減算を実現
テキストがセットされている 現在値を上書き転送する コード類・フラグ・日付もテキスト扱いを推奨
NULL または未セット(LOW-VALUE) 何もしない(スキップ) 更新不要な項目はセットしないで渡す

3-1. DELETE コード時の数値処理

DELETE (2) の場合、数値フィールドは「負の値」に変換して渡す。更新層は加算するだけなので、結果として引き算になる。

例:在庫数の更新

受注登録(CREATE): 数量 = +5 → 在庫に加算 → 在庫 100 → 105
受注削除(DELETE): 数量 = -5 → 在庫に加算 → 在庫 105 → 100

更新層は「加算するだけ」。DELETE かどうかを判断しない。

⚠ 注意点
フラグ・区分・日付など「加算で意味をなさないフィールド」はテキスト扱いにすること。DELETE 時もテキスト転送で処理し、業務ロジック側で初期値を明示的にセットして渡す。

3-2. 更新規則の擬似コード

function applyRecord(currentRow, updateRecord):
    for each field in updateRecord:
        if field.value is NULL or LOW-VALUE:
            pass  # 何もしない
        elif field.type is NUMERIC:
            currentRow[field.name] += field.value  # 加算(負値なら減算)
        else:  # TEXT / CODE / FLAG / DATE
            currentRow[field.name] = field.value   # 転送
    save(currentRow)

このシンプルさが、更新層をテーブルごとに量産できる根拠だ。ロジックは変わらず、構造だけが増える。


4. トランザクション設計

3 層構造でトランザクションをどう扱うかは、実装環境によって選択肢が異なる。現代の環境では、次のパターンが現実的だ。

4-1. 同期処理(シンプルな構成)

エントリー層 → 正規化・振分け層 → 更新層
↑──────────────────────────┘
1 トランザクション

第 2 層と第 3 層を同一トランザクション内で実行する。RDBMS のトランザクション機能をそのまま使える。多くの業務システムで最初に選ぶべき構成だ。

4-2. 非同期処理(スケールが必要な構成)

エントリー層 ─→ メッセージキュー ─→ 正規化・振分け層 → 更新層
             (イベント永続化)       (非同期コンシューマー)

イベントをメッセージキューに書き込んだ時点でエントリー層のトランザクションを完了させ、後続処理を非同期で行う。スループットが上がる一方、結果整合性の設計が必要になる。

構成 向いているケース
同期処理 強い整合性が必要な業務(在庫・残高・採番など)
非同期処理 処理量が多い・応答速度を優先したい・マイクロサービス構成
📌 冪等性(べきとうせい)を確保すること
非同期構成では、同じイベントが複数回処理されることがある。更新層が「同じレコードを 2 回受け取っても結果が変わらない」設計にしておくと、障害復旧が格段に楽になる。

5. 設計ドキュメントの作り方

この 3 層構造を採用するとき、設計の要となるドキュメントは 2 種類だ。個々のプログラム仕様書を書く前に、この 2 つを揃えることで設計の全体像が見えてくる。

5-1. イベント × テーブル更新マトリクス

横軸にテーブル名、縦軸に業務イベント名を並べ、交点に「参照(R)」「更新(W)」を記入する。

イベント ╲ テーブル 受注テーブル 在庫テーブル 売掛テーブル 商品テーブル ログ
受注登録 W W W R W
受注修正 W W W R W
受注取消 W W W W
商品マスター保守 W W

R = 参照のみ W = 更新あり (空欄)= 関与なし

💡 マスターメンテナンスも「イベント」として扱う
商品マスターの追加・変更・削除も、受注登録と同じ「業務イベント」だ。特別扱いしない。この構造ではトランザクションファイルとマスターファイルという区別をなくす。すべてはイベントによる更新である。

5-2. イベント × 項目更新定義

マトリクスで「W(更新あり)」となった交点について、どの項目をどう更新するかを定義する。

イベント テーブル 項目名 更新内容
受注登録 (CREATE) 在庫テーブル 有効在庫数 数値 / 加算 ← 受注数量(正)
受注登録 (CREATE) 売掛テーブル 売掛金額 数値 / 加算 ← 受注金額(正)
受注取消 (DELETE) 在庫テーブル 有効在庫数 数値 / 加算 ← 受注数量(負)※削除なので負値で渡す
受注取消 (DELETE) 売掛テーブル 売掛金額 数値 / 加算 ← 受注金額(負)※削除なので負値で渡す

この定義があれば、第 2 層(正規化・振分け層)の実装は機械的に書ける。属人化しない。

5-3. 「縦の標準化」と「横の拡張性」

この構造の本質的な利点は次の一点に集約される。

方向 意味
縦(構造)を標準化する 各層の責務を固定することで、どのイベントでも同じパターンで実装できる
横(機能)が変化しても テーブル追加・イベント追加はマトリクスと更新定義を追記するだけ。既存のコードに触れる範囲が限定される

「仕様変更=量的な変化」にとどめ、「構造の質的な変化」を起こさないことが目標だ。


6. よくある疑問と回答

Q1. マスターメンテナンスは「特別扱い」しなくていいのか?

しなくていい。商品マスターの登録・修正・削除も「商品マスター保守イベント」として同じ 3 層で処理する。従来の「マスターファイル」「トランザクションファイル」という区別はこの構造では存在しない。

Q2. 第 1 層でどうしても書き込みが必要な場合は?

採番管理(伝票番号の採番など)のように即時更新が必要なケースは例外として認める。ただし例外はドキュメントに明記し、その数を最小限に抑える。「例外を作りたくなったら構造を疑え」を基本姿勢にする。

Q3. 非同期構成にした場合、整合性は大丈夫か?

結果整合性(Eventual Consistency)を前提とした設計にする必要がある。重要なのは更新層の冪等性だ。同じイベントが 2 回処理されても結果が変わらない設計にしておけば、リトライ・障害復旧が安全に行える。

Q4. バッチ処理も同じ構造で扱えるか?

扱える。バッチ処理も「データ抽出(エントリー相当)→ 正規化・振分け → 更新」という 3 段セットで組み立てる考え方は有効だ。ただし、バッチ特有の大量データ処理や排他制御の考慮は別途必要になる。

Q5. この構造は「古い」のではないか?

構造の骨格は、現代のイベントソーシングや CQRS と本質的に近い。「業務イベントを受け取り、正規化し、データストアに反映する」という分離の考え方は、言語や環境が変わっても通用する。技術スタックは変わるが、問題の本質は変わっていない。

Q6. 正規化・振分け層でデータストアを参照してもよいか?

原則として、正規化・振分け層の入力はイベントレコードのみだ。しかし業務によっては、イベントレコードだけでは更新値を確定できないケースが存在する。代表例が移動平均単価の更新だ。

移動平均単価の計算には「仕入数量・仕入金額(イベントレコードにある)」と「現在の在庫数量・現在の平均単価(在庫マスターにある)」の両方が必要だ。後者はイベントレコードに含まれないため、参照なしに計算できない。

このような場合、正規化・振分け層は「値の補完」を目的に限り、データストアを読み取ることができる。これを「補完参照ルール」として標準に含める。

📌 補完参照ルール(正規化・振分け層)
イベントレコードだけでは更新値を確定できない場合、正規化・振分け層は値の補完を目的としてデータストアを読み取ることができる。ただしこの参照は(1)値の補完のみを目的とすること、(2)業務判断・条件分岐には使用しないこと、という制約を守る。この 2 条件を外れる参照が必要になった場合は、構造設計を見直すサインと考える。

Q7. これは Event Sourcing の説明ですか?内容が同じに見えます。

部分的に似ていますが、同じではない。

Event Sourcing は「イベントを永続化し、状態をイベントから再構築する」という特定のアーキテクチャパターン。本ドキュメントが扱っているのは、技術に依存しない「業務システムをシンプルに保つための構造原則」となる。

  • 業務の複雑さをどこで吸収するか
  • 更新ロジックをどう純化するか
  • 修正を「削除+追加」として扱う理由
  • 層ごとの責務分離
  • 構造を標準化し、変更を量的変化にとどめる方法

といった原則は、Event Sourcing より広い概念であり、Event Sourcing はその一部を実装として具体化したものだ。つまり、「Event Sourcing と似ている」のではなく、「Event Sourcing が本ドキュメントの原則と同じ方向を向いている」という関係になる。


7. まとめ

本ドキュメントで示した原則を一枚に整理する。

原則 内容
責務の分離 エントリー層・正規化振分け層・更新層の 3 層を明確に分け、各層の「やること・やってはいけないこと」を守る
イベントで考える すべての業務変化を「イベント(登録 / 修正 / 削除)」として表現する。マスターとトランザクションの区別をしない
更新層は判断しない 数値は加算、テキストは転送、NULL は無視。このルールだけで動く。業務ロジックは持ち込まない
縦を標準化し、横を量的変化にとどめる 構造を固定することで、機能追加・仕様変更の影響範囲を限定できる
ドキュメントは 2 種類が要 イベント × テーブル更新マトリクスと、イベント × 項目更新定義。この 2 つがあれば実装は機械的に進む
「複雑な業務」と「シンプルなコンピュータ」を繋ぐ構造は、技術が変わっても変わらない。