複雑なシステムへ降りていく五つの段階

新しいプロジェクトを初めて開いたとき、あるいは数か月ぶりに自分のコードへ戻ったとき、システムの造りをどうやってつかむだろうか。

どちらの場合も、開発者は同じ内なる地図を描くことになる。責任がどこで分けられているか、どんな可能性があり、それが具体的な実装とどうつながっているか。経験があれば主だった足がかりは早く見つかる。だがそれがアーキテクチャ自身に表れていなければ、あるいはプロジェクトやモジュールごとに変わるなら、降りていくやり方は毎回また考え出すことになる。

AOA はそのやり方を、経験ある開発者の頭からシステムの構造そのものへ移す。プロジェクトが変わっても、五つの同じ足がかりが残る。Domain、Action、Contract、Pipeline、Code。それぞれが独立した段階になり、合わさって全体像から具体的な振る舞いまでの一本道をつくる。

開発者は地図を段階的に拡大するようにこの段階を進む。まずシステムを丸ごとつかみ、それから細部を足していく。どの段階も自分の問いに完結した答えを与え、その先に何があるかを前もって知っている必要はない。

ドキュメント ↗

すべての段階の文脈が混ざっているとき

細部がすべて同時に手に入るとき、全体像を見るのを妨げているものは何か。

ふつうのリポジトリは、情報を尺度で分けてくれない。業務領域も、操作も、データモデルも、呼び出しの順序も、基盤も、具体的なコード行も、すべてが同時に並んでいる。作業記憶は違う層の事実を抱え、そのあいだの関係を自力で組み直さねばならない。注意はシステムの造りと目の前の細部のあいだを行き来し続け、だから理解は中断のあとすぐ崩れる。

全体像がないことは仕事にどう響くか

以下では同じ情報の集まりを二つの状態で示す。左は混ざったまま、右は五つの連なる問いに並べ替えたもの。まず最初の問いから。システムのどこに責任は住んでいるのか。

平らなコードベース · 足がかりのない新しい事実
orders.pyBillingServiceapi.pycreate_orderreserve_stockOrderModelsend_mailutils.pydb.py
五つの段階 · 文脈が順に組み上がる
01Domain責任はどこに住んでいるか
02Actionシステムには何ができるか
03Contractその操作は何を求め、何を約束するか
04Pipeline筋書きはどう運ばれるか
05Codeその具体的な振る舞いはどこにあるか
ドキュメント全文 ↗

まずは責任の地図

責任はどこに住んでいるか。

最初の足がかりになるのが責任の地図だ。コントローラも、サービスも、データモデルも、基盤のアダプタも、呼び出しの連なりも、システムの大きな領域のまわりに集める — のちに操作とデータとコードを結びつけていける骨組みへ。

Domain はシステムを技術的な層でではなく、変わりにくい責任の領域で分ける。注文、決済、コミュニケーション、分析。等しく重要に見える何百ものファイルの代わりに、初めて見渡せる地形が現れる。

Domain の地図がもたらすもの

図では四つの Domain がアプリケーションの最上層をなしている。この先は StoreDomain を順に開いていく。まずその行為を見て、次に一つの操作の契約、その筋書き、そして具体的な実装へ。

4 × ActionStoreDomain

カート、注文、配送への引き渡しを受け持つ

3 × ActionBillingDomain

支払い、請求書、返金を受け持つ

3 × ActionMessagingDomain

顧客とのやり取りと webhook を受け持つ

3 × ActionAnalyticsDomain

イベント、集計、業務レポートを受け持つ

ドキュメント全文 ↗

地図が可能性の一覧へ変わる

システムには何ができるか。

Domain の地図は責任がどこにあるかを示すが、システムに何ができるかはまだ語らない。そのために、それぞれの Domain が行為の一覧として開かれる — その Domain がシステムの他の部分へ差し出す、名前のついた可能性だ。

Action とは Domain の名前のついた公開された可能性である。一覧が見せるのは内部の関数ではなく、Domain が他の部分へ差し出す操作と、そのあいだの向きのあるつながりだ。どの行為も単一の入口になる。呼ぶ側は必要な可能性を意味で選び、ばらばらのメソッドから操作を組み立てずに済む。

行為の一覧がもたらすもの

図では CreateOrderAction が強調されている — 注文の作成を始める行為だ。そばには StoreDomain の他の行為と、そのあいだのつながりが並ぶ。ある操作が別の操作に乗ることはあるが、依存には向きがあり、輪にはならない。この段階で見えるのは行為の名前とつながりだけ。入力、結果、筋書き、コードはこの先で開かれる。

StoreDomain名前のついた実行可能な可能性
CreateOrderAction

注文を作る:検証し、確保し、支払う

→ ChargePaymentAction

GetOrderAction

注文の現在の状態を取得する

CancelOrderAction

注文を取り消し、返金を始める

→ RefundPaymentAction

ShipOrderAction

確定した注文を配送へ引き渡す

ドキュメント全文 ↗

操作の境界を確定する

その操作は何を求め、何を約束するか。

可能性にはもう名前とシステム内の居場所がある。それを「中身の見えない箱」として使うには、どんなデータを受け取り、どんな結果を返す義務があるのかを正確に知る必要がある。

Contract は Action の境界を、型のついた曖昧さのないものにする。Params は操作が受け取るすべてを、Result は返す義務のあるすべてを言い表す。呼ぶ側のコードが頼るのはこの約束であって、Action の内側の造りではない。入口と出口と外から見える振る舞いに互換性が保たれているかぎり、実装は変えてよい。

行為の契約がもたらすもの

見知った CreateOrderAction が完全な署名を得る。左に CreateOrderParams、右に CreateOrderResult。フィールドはもう抽象的なデータモデルとして存在するのではない。居場所と役目のはっきりした、具体的な操作に属している。それを使うのに Action の内側は相変わらず要らない。

CreateOrderParams
customer_id: str
items: list[Item]
currency: str
CreateOrderAction

宣言された一つの境界の内側で、注文の作成を統率する

Params → Result
CreateOrderResult
order_id: str
payment_id: str
status: OrderStatus
ドキュメント全文 ↗

操作を筋書きへ展開する

筋書きはどう運ばれるか。

契約は操作の始まりと終わりを確定するが、一方がどうやってもう一方になるのかは見せない。そのために、業務上の筋書きの内側の運びを開く。

Pipeline は主たる業務の筋書きを一本道にして見えるようにする。入力を確かめ、商品を確保し、支払いを通し、結果を組み立てる。現実に誤りも分岐も補償もないという意味ではない。それらが主線に対してはっきりした居場所を得るということだ。どの手順も自分の局所的な契約を持ち、Params と State と Context から何を読むか、次の段階のために State へ何を足すかを宣言する。

操作の筋書きがもたらすもの

Domain が筋書きに居場所を与え、Action が名前を、Contract が始まりと約束された結末を与えた。Pipeline が加えるのは因果の順序と内側の境界だ。reserve_inventory の手順は独立した状態の変化として切り出されている。すでに確かめられたデータを受け取り、筋書きの続きのために reservation_id を残す。

regularvalidatevalidated_items
regularreserve_inventoryreservation_id
regularcharge_paymentpayment_id
summarycreate_resultOrderResult
ドキュメント全文 ↗

ここでようやく、具体的な振る舞い

その具体的な振る舞いはどこにあるか。

Pipeline は必要な振る舞いの正確な居場所を示し、探す範囲を一気に狭める。もはやリポジトリ全体でも Action 全体でもなく、目的も入口も出口も分かっている一つの手順の実装を開けばいい。

Code が五番目の段階になるのは、細部が重要でないからではない。いまやそれが意味に囲まれているからだ。この振る舞いがどの Domain のものか、Action がどんな可能性を実現するか、Contract が何を約束するか、reserve_inventory が Pipeline のどこに座るかは、すでに分かっている。実装は具体的な課題への局所的な答えとして読める。リポジトリという終わりのない捜査への入口としてではなく。

振る舞いを一つの手順に切り離すことがもたらすもの

reserve_inventory を開くとき、その意図も入口も筋書きの中の居場所も、あらかじめ分かっている。原則は変わらない。外への作用は宣言された Resource と公開された Action を通らねばならない。アーキテクチャの文法は逸脱を見えるものにし、検査できるものにする — 説明の文章だけであらゆる迂回を物理的に禁じられるふりはせずに。

store/actions/create_order.py
@regular_aspect("Reserve inventory")@result_string("reservation_id", required=True)@context_requires("user.tenant_id", "env.inventory_region")async def reserve_inventory_aspect(    self, params, state, box, connections, ctx):    inventory = box.resolve(InventoryResource)    reservation_id = await inventory.reserve(        tenant_id=ctx.get("user.tenant_id"),        region=ctx.get("env.inventory_region"),        items=state["validated_items"],    )    return {"reservation_id": reservation_id}
$ uv run python store/actions/create_order.py
出力
ドキュメント全文 ↗

五つの段階はコードを隠しもしないし、システムを図に単純化もしない。全体のモデルから細部までの道すじを保つ。新人は文脈を少しずつ組み立て、アーキテクトは一つ一つのファイルではなく文法と境界を扱い、AI エージェントは定められた規則の内側で局所的な仕事をする。人がシステムの言語を設計し、機械がその言語で振る舞う。

複雑なシステムへ降りていく五つの段階 · aoa.run