― 複雑な業務をシンプルに扱う 3 層構造と更新規則 ―
エンジニア向け設計思想ガイド
対象読者:業務システム開発に携わるすべてのエンジニア
0. はじめに ― なぜシステムは修正のたびに壊れるのか
あなたは、こんな経験をしたことはないだろうか。
- 小さな仕様変更のつもりが、まったく別の画面でバグが出た。
- 同じような処理なのに、担当者によって書き方がバラバラで、読解に時間がかかる。
- 「ここを直せばいい」と思ったのに、直す場所が 5 か所もあった。
これらは「スキル不足」ではない。構造の問題だ。
業務システムには、二つの性質がある。
| 要素 | 性質 |
|---|---|
| 業務ルール | 複雑で、頻繁に変わる |
| コンピュータがやること | データの追加・更新・削除という単純な操作の繰り返し |
この「複雑な業務」と「シンプルなコンピュータ」をどう繋ぐか。それを整理するのが、本ドキュメントの目的だ。
言語やフレームワークに依存しない、業務システム設計の原則を示す。現代のアーキテクチャで実装する際の指針として活用してほしい。
1. 3 層構造の概要
業務システムの処理は、次の 3 つの責務に分けて設計する。
世間で「3 層構造」というと、ふつうは「プレゼンテーション層(画面)・ビジネスロジック層・データ層」の分け方を指す。本ドキュメントの 3 層はそれとは別物で、データを更新する処理の流れを「受け取る・変換して振り分ける・反映する」の 3 段に分けたものだ。両者は対立せず、画面を持つシステムなら、本書の第 1 層がプレゼンテーション層の背後に位置する形になる。
| 層 | 名称 | 責務 |
|---|---|---|
| 第 1 層 | エントリー層 | 業務イベントを受け取り、入力検証を行い、正規化されたデータを次層へ渡す |
| 第 2 層 | 正規化・振分け層 | 受け取ったデータを各ファイル(テーブル)への更新レコードに変換・振り分ける |
| 第 3 層 | 更新層 | 渡されたレコードを、決められたルールだけでデータストアに反映する |
1-1. 構造の流れ
|
更新層
テーブル A
|
更新層
テーブル B
|
更新層
テーブル C
|
更新層
テーブル D ・・・
|
第 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 層(更新) | 差分更新を行うリポジトリ層(例:UPDATE … SET 数量 = 数量 + ?) ※値を上書きする upsert とは異なる。加算は差分更新、転送は上書きで実装する |
3. 更新層の更新規則
第 3 層(更新層)は、「判断しない」ことが最大の特徴だ。渡されたレコードの各フィールドを、次のルールだけで処理する。
| フィールドの状態 | 処理 | 補足 |
|---|---|---|
| 数値がセットされている | 現在値に加算する | DELETE コードの場合は負の値を渡すことで減算を実現 |
| テキストがセットされている | 現在値を上書き転送する | コード類・フラグ・日付もテキスト扱いを推奨 |
| NULL または未セット(LOW-VALUE) | 何もしない(スキップ) | 更新不要な項目はセットしないで渡す |
3-1. DELETE コード時の数値処理
DELETE (2) の場合、数値フィールドは「同じ内容を CREATE したときに渡す値」の符号を反転して渡す。更新層は加算するだけなので、CREATE で加えた分がそのまま取り消される。符号の反転は第 2 層(正規化・振分け層)の仕事で、更新層は DELETE かどうかを知らない。
例:在庫数の更新
受注登録(CREATE): 数量 = -5 → 有効在庫に加算 → 在庫 100 → 95
(受注数は有効在庫から差し引くという業務ロジックに応じて第2層、正規化・振分けで負数に置き換える)
受注削除(DELETE): 数量 = +5 → 有効在庫に加算 → 在庫 95 → 100
(登録時に渡した -5 の符号を、第2層、正規化・振分けで反転して +5 として更新層に渡す)
更新層は「加算するだけ」。DELETE かどうかを判断しない。
⚠ 注意点
フラグ・区分・日付など「加算で意味をなさないフィールド」はテキスト扱いにすること。DELETE 時もテキスト転送で処理し、業務ロジック側で初期値を明示的にセットして渡す。
3-2. 行の作成と削除
加算・転送・無視の規則は、すでに存在する行の「項目」をどう変えるかの規則だ。行そのものをいつ作り、いつ消すかも、次のように決めておく。
| 場面 | 規則 |
|---|---|
| 更新レコードのキーに該当する行がない | 数値項目を 0、テキスト項目を初期値とした行を作り、そのうえで通常の規則を適用する |
| 行を物理的に消したい | 更新層は自分の判断で行を消さない。第 2 層が「行削除」を示す制御コード付きの更新レコードを明示的に渡したときだけ削除する(または削除フラグをテキスト転送で立てる) |
こうしておけば、「在庫が 0 になったら行を消すのか」といった業務判断が更新層に入り込まない。
3-3. 更新規則の擬似コード
function applyRecord(updateRecord):
if alreadyProcessed(updateRecord.eventId): # 重複処理の防止(4-3 参照)
return
currentRow = find(updateRecord.key) or newRowWithInitialValues(updateRecord.key)
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)
markProcessed(updateRecord.eventId) # save と同じトランザクションで記録
このシンプルさが、更新層をテーブルごとに量産できる根拠だ。ロジックは変わらず、構造だけが増える。
4. トランザクション設計
3 層構造でトランザクションをどう扱うかは、実装環境によって選択肢が異なる。現代の環境では、次のパターンが現実的だ。
4-1. 同期処理(シンプルな構成)
エントリー層 → 正規化・振分け層 → 更新層 ↑──────────────────────────┘ 1 トランザクション
第 2 層と第 3 層を同一トランザクション内で実行する。RDBMS のトランザクション機能をそのまま使える。多くの業務システムで最初に選ぶべき構成だ。
4-2. 非同期処理(スケールが必要な構成)
エントリー層 ─→ メッセージキュー ─→ 正規化・振分け層 → 更新層
(イベント永続化) (非同期コンシューマー)
イベントをメッセージキューに書き込んだ時点でエントリー層のトランザクションを完了させ、後続処理を非同期で行う。スループットが上がる一方、結果整合性の設計が必要になる。
| 構成 | 向いているケース |
|---|---|
| 同期処理 | 強い整合性が必要な業務(在庫・残高・採番など) |
| 非同期処理 | 処理量が多い・応答速度を優先したい・マイクロサービス構成 |
4-3. 重複処理への備え(冪等性)
非同期構成やリトライのある環境では、同じイベントが 2 回以上届くことがある。ここで注意が必要なのは、「数値は加算」という更新規則は、それだけでは冪等にならないことだ。同じレコードを 2 回受け取れば、2 回加算されてしまう。
そこで、次の仕組みを組み合わせる。
- 第 1 層で、イベントごとに一意のイベント ID を付ける(修正の DELETE+CREATE は、それぞれ別の ID を持つ)
- 更新層は、処理済みのイベント ID を記録するテーブルを持ち、更新と同じトランザクションで ID を書き込む
- 処理済みの ID が再び届いたら、何もせずに成功として返す
加算は順番を入れ替えても結果が同じになる。そのため、複数のイベントが同じ行をほぼ同時に更新しても、「1 件ずつ加算する」ことさえ守れば矛盾した結果になりにくい。現在値を読んで計算し直して書き戻す方式に比べて、排他制御の設計が単純になる。これは「更新層は加算するだけ」という原則の大きな利点だ。
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 層(正規化・振分け層)と第 3 層(更新層)の実装はほぼ機械的に書ける。属人化しにくく、定義から AI でコードを自動生成するのにも向いた構造になる。
5-3. 「縦の標準化」と「横の拡張性」
この構造の本質的な利点は次の一点に集約される。
| 方向 | 意味 |
|---|---|
| 縦(構造)を標準化する | 各層の責務を固定することで、どのイベントでも同じパターンで実装できる |
| 横(機能)が変化しても | テーブル追加・イベント追加はマトリクスと更新定義を追記するだけ。既存のコードに触れる範囲が限定される |
「仕様変更=量的な変化」にとどめ、「構造の質的な変化」を起こさないことが目標だ。
6. よくある疑問と回答
Q1. マスターメンテナンスは「特別扱い」しなくていいのか?
しなくていい。商品マスターの登録・修正・削除も「商品マスター保守イベント」として同じ 3 層で処理する。従来の「マスターファイル」「トランザクションファイル」という区別はこの構造では存在しない。
Q2. 第 1 層でどうしても書き込みが必要な場合は?
採番管理(伝票番号の採番など)のように即時更新が必要なケースは例外として認める。ただし例外はドキュメントに明記し、その数を最小限に抑える。「例外を作りたくなったら構造を疑え」を基本姿勢にする。
Q3. 非同期構成にした場合、整合性は大丈夫か?
結果整合性(Eventual Consistency)を前提とした設計にする必要がある。重要なのは重複処理への備えだ。加算規則はそのままでは冪等ではないので、イベント ID で処理済みかどうかを判定する仕組み(4-3)を必ず組み合わせる。これがあれば、リトライや障害復旧を安全に行える。
Q4. バッチ処理も同じ構造で扱えるか?
扱える。バッチ処理も「データ抽出(エントリー相当)→ 正規化・振分け → 更新」という 3 段セットで組み立てる考え方は有効だ。ただし、バッチ特有の大量データ処理や排他制御の考慮は別途必要になる。
Q5. この構造は「古い」のではないか?
「業務イベントを受け取り、正規化し、差分としてデータストアに反映する」という分離の考え方は、現代のイベント駆動アーキテクチャと同じ方向を向いており、言語や環境が変わっても通用する。技術スタックは変わるが、問題の本質は変わっていない。
Q6. 正規化・振分け層でデータストアを参照してもよいか?
原則として、正規化・振分け層の入力はイベントレコードのみだ。しかし業務によっては、イベントレコードだけでは更新値を確定できないケースが存在する。代表例が移動平均単価の更新だ。
移動平均単価の計算には「仕入数量・仕入金額(イベントレコードにある)」と「現在の在庫数量・現在の平均単価(在庫マスターにある)」の両方が必要だ。後者はイベントレコードに含まれないため、参照なしに計算できない。
このような場合、正規化・振分け層は「値の補完」を目的に限り、データストアを読み取ることができる。これを「補完参照ルール」として標準に含める。
イベントレコードだけでは更新値を確定できない場合、正規化・振分け層は値の補完を目的としてデータストアを読み取ることができる。ただしこの参照は(1)値の補完のみを目的とすること、(2)業務判断・条件分岐には使用しないこと、という制約を守る。この 2 条件を外れる参照が必要になった場合は、構造設計を見直すサインと考える。
Q7. これは Event Sourcing の説明ですか?内容が同じに見えます。
部分的に似ていますが、同じではない。
Event Sourcing は「イベントそのものを正の記録として永続化し、現在の状態はイベントを再生して組み立てる」アーキテクチャパターンだ。一方、本ドキュメントの構造では、正の記録はテーブルの現在値であり、イベントは更新のために通過していくもの(ログとして残すことはあっても、状態の再構築には使わない)という違いがある。
本ドキュメントが重視しているのは、技術に依存しない次のような構造原則だ。
- 業務の複雑さをどこで吸収するか
- 更新ロジックをどう純化するか
- 修正を「削除+追加」として扱う理由
- 層ごとの責務分離
- 構造を標準化し、変更を量的変化にとどめる方法
これらは Event Sourcing を採用するかどうかとは独立に使える。両者は「業務の変化をイベントとして扱い、更新を単純に保つ」という点で同じ方向を向いた考え方であり、Event Sourcing を採用するシステムでも、本書の層の分け方や更新規則はそのまま参考になる。
7. まとめ
本ドキュメントで示した原則を一枚に整理する。
| 原則 | 内容 |
|---|---|
| 責務の分離 | エントリー層・正規化振分け層・更新層の 3 層を明確に分け、各層の「やること・やってはいけないこと」を守る |
| イベントで考える | すべての業務変化を「イベント(登録 / 修正 / 削除)」として表現する。マスターとトランザクションの区別をしない |
| 更新層は判断しない | 数値は加算、テキストは転送、NULL は無視。このルールだけで動く。業務ロジックは持ち込まない |
| 重複に備える | 加算はそのままでは冪等ではない。イベント ID で処理済みを判定し、同じイベントを二度反映しない |
| 縦を標準化し、横を量的変化にとどめる | 構造を固定することで、機能追加・仕様変更の影響範囲を限定できる |
| ドキュメントは 2 種類が要 | イベント × テーブル更新マトリクスと、イベント × 項目更新定義。この 2 つがあれば実装は機械的に進む |
