🚦

ワークフロー管理の設計

2024/12/15に公開
  • システムを作っていると至る所に状態遷移(ワークフロー)が出てくる
    • 例えば、申請承認ワークフローだったり、工場の生産ライン管理だったり
  • ここでは EC サイトの注文、決済、出荷のような一連の流れを例にとって、それをシステム上どのように管理するかについて考える

ステータスカラムで管理する

  • 注文テーブルかどこかに注文状況ステータスカラムを用意して、アップデートしていく

  • status カラムの値が1なら注文済み、2なら支払い済み、3なら出荷済み、とみなすイメージ
  • 雑な方針であるが、ミニマムにやるならオーソドックスな形
  • アップデートする際のルールはアプリケーション側で管理する
    • ステートマシン的なライブラリが使えて実装上はお手軽
    • Rails なら例えば AASM のような gem でイベントを enum 定義して、各イベントに遷移するメソッドを実装する、みたいなイメージ
  • データとしての堅牢性は落ちる
    • 実装をミスると支払いがないのにいきなり出荷状態にできたりする
    • いつどのイベントが起きたのかが追えなくなる
  • ステータスカラムの目的が曖昧すぎる
    • なんのステータスを管理しているのかわかりづらい
    • 魔境になりがち
  • それぞれのイベントの発生時刻なども残したいとなると paid_atshipped_at などのカラムも欲しくなるが、ステータスカラムとの整合性を担保しないといけなくてバグりやすい
    • shipped_at が埋まっているのにステータスが出荷済みになっていなかったり

データベースにイベントごとのテーブルを作って管理する

  • イベントごとのテーブル、orderspaymentsshipments テーブルを作って管理する

  • 単純なステートマシンは使えないので、状態の特定を自分でロジック書いて実装する
    • orders を取る際に payments テーブルと shipments テーブルもジョインして、payments があるかつ shipments がない場合は支払い済み、paymentsshipments もあれば出荷済みと認識できる
  • ステータスカラムがなくなり情報が正規化される
    • ステータスが paid なのに paid_at がない、みたいな不整合が起きなくなる

テーブル同士の外部キー制約でより固くする

  • 上記の場合、支払いがないのに出荷がある、という状態がデータ上は起こりうる
  • もし、注文→支払い→出荷というフローがその世界の真理の場合、外部キー制約をうまく使ってフローをよりロバストに管理することができる
  • フローの下流のテーブルは自分の手前のテーブルに対して外部キー制約を持つようにする

  • このようにすると、payments レコードがないのに shipments レコードを作ることは物理的に不可能になるので、ワークフローの順序性をデータベースの制約によってより固く管理できる
  • ただし、世界の真理が崩れてワークフローが変わるときに変更が大変
    • 例えば、得意客の場合は決済を後払いにして先に出荷できる、という要件が出てきた際に、データモデルから大幅に変更する必要がある
  • ソフトウェアの良いところはソフトで変更容易性が高い点にあるので、なんでも固ければ固いほどいいというものでもない
    • ロバストネスだけを考えると、データベースの制約でガチガチにしたくなるかもしれないが、本当にそれでいいんだっけは立ち止まって考える必要がある
  • また、状態を特定するために全部のテーブルをジョインしないといけないため、セレクトコストが高くなる

Raw イベントを保存して、イベントソーシングで状態を特定する

  • 注文イベントテーブルを用意して、そこに注文に関するイベントをどんどん放り込んでいく

  • その注文が今どういう状態なのかはそのイベントログからリプレイする
    • 例えば、paid イベントがあれば決済が完了しているし、shipped イベントがあれば出荷も完了している、みたいな
  • イベントの種類でいかようにも状態を定義できるので拡張性が高い
    • 例えば、決済キャンセルを管理できるようにしたい場合、決済キャンセルイベントを定義してイベントととして保存すればよい
  • イベント同士の依存関係(支払いがないと出荷できないなど)はアプリケーション側で管理する必要がある
  • イベントの数が多かったり、イベントテーブル自体のレコード数が増えてくるとリプレイコストが高くなっていくので、どこかに状態のキャッシュを持ちたくなるかもしれない

汎用ワークフロービルダーを作って、設定でフローを定義できるようにする

  • 申請承認ワークフローなんかはユースケースによってかなり要求が変わるので決め打ちでシステム化するのが難しい
    • 多段階承認
    • 複数人承認
      • 必須承認者の設定
    • 差し戻し
      • 多段階の場合どこから承認をやり直すか
    • 承認者選択
    • etc
  • こういうのに対応した汎用ワークフローを作るには、申請内容のマスタ定義ができる機能と、それごとのワークフローを柔軟に設定できる汎用的なビルダーの設計が必要になってくる
  • このような設計はデザインパターン界隈で既に多くのサンプルがありそうなのでここでは割愛する

まとめ

  • この種の「どこまで固く作るか」、「どこまで柔軟性を持たせるか」という品質特性の設計は通常エンジニアリングの世界の話なので、エンジニアの手腕が問われる
    • 状態遷移図やイベント、トリガーの定義を元にビジネスサイドとは要件をすり合わせると思うが、その結果をシステム的にどのように実現するか、はエンジニアの設計に依存しがち
    • サービスの走り出しの時はワークフローのゴールデンパスの定義だけがある状態からスタートすることが多いが、実際に利用者が増えると多種多様なユースケースが見えてきて、当初の想定フロー以外もサポートする必要性が出てきたりする
    • もしガチガチに固く作る場合、今見えている要件のフローがどの程度不変的なものなのか、という定義が重要
    • どこまでの未来を見据えて、どのぐらい固く作るかの費用対効果を考える
  • エンジニアが業務ドメインを理解せずシステム目線で作ると間違った品質特性の判断をして設計してしまう恐れがある
    • 過度に固く作りすぎて後々の変更コストが高くついてしまったり、イベントの依存関係がシステム制約になりビジネスの変化に対応しづらくなったり
  • エンジニア(アーキテクト)がしっかりとドメインエキスパートレベルの知識を持ち、正しい判断を下せるようになろう

  • この記事の例ではワークフローに焦点を当てるため、かなりシンプルな例を扱った
    • 実際は、複数の注文を束ねて一回で出荷したいとか、決済キャンセルとか、もっと複雑な要件が色々と出てきうるのでより混迷を極めると思う
    • もっというとそれぞれのリソース、イベントをどれぐらいの結合度で作るか(クラスを疎結合にするか、サービス単位で分けてしまうかなどなど)からモデリングする必要がある場合がある

Discussion