TypeScriptで設計する 堅牢さとUXを両立した非同期ワークフローの実現
by
Matsumoto Moeka
×
Copy
Open
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
同期APIの壁を越える TypeScriptで設計する 堅牢さとUXを両立した 非同期ワークフローの実現 アセンド株式会社 松本 萌花
Slide 2
Slide 2 text
moeka__c m-moeka 松本 萌花 Matsumoto Moeka 経歴 SIerでBEエンジニア → 2022年5月〜 アセンド株式会社に4人目エンジニアとして入社 現在はリードプロダクトエンジニア。 0->1案件に単身でぶち込まれがち。 なんだかんだ好きなのはBE。 型パズルってやりだすと結構ハマる。 最近犬を飼い始めた(超可愛い)。
Slide 3
Slide 3 text
2020年創業 シリーズB 社員 35名 エンジニア 12名 物流の真価を開き あらゆる産業を支える トラック運送業向けSaaSを開発 11月13日 シリーズB調達 プレスリリース
Slide 4
Slide 4 text
協力会社連携により運送会社を繋ぎ、 物流デジタルネットワークを構築 オールインワン SaaS から トラック運送会社の標準OSへ 運行管理 案件管理 ドライバー アプリ 請求管理 車両管理 労務管理 帳票管理 経営管理
Slide 5
Slide 5 text
DF | 4.91 /day L T | 2 hours MTTR | 24 min CFR | 2.6% Full-Stack TS で1エンジニアの生産性を高め、 プロダクトエンジニアがオーナーシップを持ってオールインワン SaaS を開発する Product Engineering 技術 デザイン 事業 Full-Stack TypeScript Full-Stack TypeScript のプロダクト組織
Slide 6
Slide 6 text
大規模・複雑化していくWebシステムは、適切なモジュール化・疎結合化が必須ですが 一方で分割したものを連動させることの技術的難易度とのトレードオフになります。 事業拡大に伴いシステムへの要求が拡大していく中で、 同期連携から非同期連携へのリアーキテクチャに踏み切りました。 この事例を通した、実践の中で培った設計上の考え方と 実装していく上での TypeScript の型の力の活用方法を紹介します。 本日お話すること
Slide 7
Slide 7 text
AGENDA 同期 API の壁 非同期データ連携方式設計 TypeScript での実装 1 2 3
Slide 8
Slide 8 text
1 同期 API の壁
Slide 9
Slide 9 text
支払い管理 車両管理 労務管理 ● ● ● ドライバー アプリ 請求管理 運行管理 案件管理 業務間連携 協力会社 依頼 テナント間連携 発注 テナント間連携 荷主企業 ロジックスはオールインワンSaaSとして、運送業の中心・運行業務をハブとして、 様々な業務モジュールが連動しあい、全体の業務進行を支えている。 オールインワン SaaS の特性
Slide 10
Slide 10 text
労務管理 ● ● ● ドライバー アプリ 請求管理 運行 案件管理 業務間連携 発注 テナント間連携 荷主企業 請求 請求書へ集約 入金確認 案件 配車 運行終了 運賃確定 発注 依頼 実績確認 支払い 請求書確認 修正 修正 例えば、案件 → 請求連携の場合 オールインワン SaaS の特性 テナント、コンテキストを越えた多くの連携 強い整合性要件(金額ズレ不可、バッチ処理不可)
Slide 11
Slide 11 text
とにかく連携が大変。 業務としては異なるため、コンテキストを分離して疎結合に作りたい コンテキストを跨いでも、即時に正確な情報を連携する必要がある
Slide 12
Slide 12 text
オールインワン SaaS の特性 同一ブランドで提供するからこそ 業務間でもシームレスなUX ユーザーの声などから素早い改善 深く・正しく・シンプルに業務を実現 他の業務に及ぼさず 深さと拡張性を追求できる 疎結合・高凝集なモジュール設計 業務データを相互に連携。 基幹業務を最高効率で回すための 即時性と整合性が高い情報連携設計 vs. 複雑な業務 vs. 多種多様な業務 プロダクト要求 求められる システム要件 ポイントソリューションではなくオールインワンであることが競合優位性。 両立の難易度は高く、事業のスピードは早いが、適切に取り組まねばならない。 エンジニアには設計時のバランス感覚が求められる。
Slide 13
Slide 13 text
オールインワン SaaS の特性 同一ブランドで提供するからこそ 業務間でもシームレスなUX ユーザーの声などから素早い改善 深く・正しく・シンプルに業務を実現 他の業務に及ぼさず 深さと拡張性を追求できる 疎結合・高凝集なモジュール設計 業務データを相互に連携。 基幹業務を最高効率で回すための 即時性と整合性が高い情報連携設計 vs. 複雑な業務 vs. 多種多様な業務 プロダクト要求 求められる システム要件 ポイントソリューションではなくオールインワンであることが競合優位性。 両立の難易度は高く、事業のスピードは早いが、適切に取り組まねばならない。 エンジニアには設計時のバランス感覚が求められる。 プロダクト初期は同期 API で対応 コンテキストは分けてなるべく疎結合にしつつ 整合性担保のため 1 つのトランザクションで同期的にデータ連携
Slide 14
Slide 14 text
同期 API の壁 事業拡大に伴い、連携の種類が多様に&技術特性要件が変化した結果、 現状のアーキテクチャが限界を迎えた システム課題 事業 変化 詳細 概要 業務内で取り扱うデータの変化 テナント間の情報連携 大量データ 営業所が全国に。 CSが面倒見きれないので 処理状態やエラーFBのUX向上が急務 変更通知、変更履歴 整合性要件の複雑さ イベントが下請けチェーンを連鎖する 同期APIでのバルク処理でロック地獄に よいUXを実現するためのコードが複雑化 全ての要求を愚直に実現するのはカオス 下請けのテナント含めて整合性を取る必 要性など、同期的な設計の限界
Slide 15
Slide 15 text
整合性 堅牢性 UX 拡張性 整合性・堅牢性 データの正しさ バグの少なさ コードの見通しの良さ UX・拡張性 パフォーマンスの良さ 処理状況のわかりやすさ サービスの独立性 整合性とUXを両立するアーキテクチャに刷新を目指す アーキテクチャの刷新を決定
Slide 16
Slide 16 text
2 非同期データ連携方式設計
Slide 17
Slide 17 text
今回は TSKaigi なのでポイントを絞って
Slide 18
Slide 18 text
イベントを中心としたアーキテクチャへの転換 状態そのものではなく、「どんなイベントが起きて、 現在の状態に至ったか」を記録 データの状態を「イベントの履歴」から再構築 (※ 今回はスナップショットの状態も保存する形式) システム全体の構成を「イベント」を媒介に疎結合化 「何かが起きた(イベント)」という事実を発火し、 それを受け取るコンポーネントが非同期的に処理 イベントソーシングパターン イベント駆動パターン 「1つのトランザクションによる同期 API での連携」から 「イベントソーシング + イベント駆動」の方針に決定 ドメインイベントを介した情報伝達によりサービスを疎結合に イベントを通して正確に情報連携できる基盤を作る 履歴や通知との相性も良い
Slide 19
Slide 19 text
請求 ? 案件 Q3. どうやってイベントから呼び出すか Q2. どうイベントを発行するか Q1. どういうワークフローを組むか 検討した 3 つの問い イベントを中心としたアーキテクチャへの転換
Slide 20
Slide 20 text
イベントを中心としたアーキテクチャへの転換 請求 ? 案件 Q3. どうやってイベントから呼び出すか Q2. どうイベントを発行するか Q1. どういうワークフローを組むか
Slide 21
Slide 21 text
Q1. ワークフロー|Saga パターン Sagaパターンとは、複数のサービス(業務)に またがるデータの一貫性を保つための設計パターン 複雑な業務を「ローカルトランザクションの連続」として扱う もし途中で処理が失敗した場合、それまでに行われた処理を打ち消すための処理 (補償トランザクション)を逆順に実行 図引用:https://learn.microsoft.com/ja-jp/azure/architecture/patterns/saga
Slide 22
Slide 22 text
Q1. ワークフロー|Saga パターン Sagaフロー制御の実装方式は大きく2種類 フロー制御が各サービスに分散 各サービスがイベントを購読して自律的に反応し 連鎖する フローを制御するコンポーネントが存在 Sagaの状態を管理し全体の流れを明示的に定義 コレオグラフィ方式 オーケストレーション方式 図引用:https://learn.microsoft.com/ja-jp/azure/architecture/patterns/saga
Slide 23
Slide 23 text
Q1. ワークフロー|Sagaパターン コレオグラフィ方式 ■ 状態管理 ■ 拡張性 シンプルな連携や疎結合を優先したい連携向き 各サービスが自分のローカル状態(イベント履歴)のみ持つ 進行状態は発生したイベント群を追うしかなく 補償トランザクションも合わせた全体整合性の把握は難しい 新サービスを「イベントをトリガに参加させる」だけ 疎結合でスケーラブル https://learn.microsoft.com/ja-jp/azure/architecture/patterns/saga
Slide 24
Slide 24 text
Q1. ワークフロー|Sagaパターン https://learn.microsoft.com/ja-jp/azure/architecture/patterns/saga オーケストレーション方式 状態管理が複雑な連携や可観測性を一箇所にまとめたい連携向き ■ 状態管理 ■ 拡張性 状態マシンを中央で持ちDB等に進行状況が記録される 失敗監視やリトライポリシー統一も容易 補償トランザクションも順序を完全制御できて安全 ステップ追加→オーケストレーターに組み込み 強い中央性がある
Slide 25
Slide 25 text
Q1. ワークフロー|Sagaパターン 今回は、連携の特性に応じて コレオグラフィとオーケストレーションを併用する形を採用。 コレオグラフィ方式の対象 発生した業務イベントは全てPublish 簡単な連携なら これからどんどん追加していけるように オーケストレーション方式では実現しにくい 連携チェーンの実装予定もあったため オーケストレーション方式の対象 要件が複雑なユースケースに限り、 中央で状態管理をしながら処理 障害時の追跡性や、 処理状況のユーザーFBの容易性を重視するもの
Slide 26
Slide 26 text
イベントを中心としたアーキテクチャへの転換 請求 ? Q3. どうやってイベントから呼び出すか Q1. どういうワークフローを組むか 案件 Q2. どうイベントを発行するか
Slide 27
Slide 27 text
シナリオ① DB成功 イベント送信失敗 データはあるのに 通知が届かない ↓ 不整合 データがないのに 通知が届いてしまう ↓ 誤作動 シナリオ② DB成功 イベント送信失敗 Q2. イベント発行 | 課題 DBトランザクションとイベント送信のどちらか一方が失敗する可能性がある
Slide 28
Slide 28 text
Q2. イベント発行|Outboxパターン Outbox(送信箱)パターンによる信頼性の高いイベント発行を採用。 「少なくとも1回(at least once)」が保証される 更新された集約の状態と業務イベントの両方を 一つのトランザクションとしてデータベースに コミットする イベント発行サービスは新規にコミットされた 業務イベントを読み取る イベント発行サービスは読み取った 業務イベントをイベント基盤に発行する
Slide 29
Slide 29 text
イベントを中心としたアーキテクチャへの転換 ? Q1. どういうワークフローを組むか 案件 Q2. どうイベントを発行するか 請求 Q3. どうやってイベントから呼び出すか
Slide 30
Slide 30 text
Q3. コマンド呼び出し|課題 Outboxパターンの性質は 「少なくとも1回(at least once)」 同一イベントを複数回配信 してしまう可能性がある。 イベント送受信を担う キューイングサービスのレイヤーでも 「厳密に1回」は難しいことがほとんど 例)AmazonSQSにおいて VisibilityTimeout内に処理が完了しなかった場合 イベントの重複配信問題
Slide 31
Slide 31 text
Q3. コマンド呼び出し|冪等性の担保 アプリケーション上で冪等性を担保する 例1) コマンド自体を冪等に作る 例2) Idempotency-Keyでハンドラを冪等にする ※Idempotency-KeyにはeventId等を利用 コマンド自体を冪等に作る Idempotency-Keyで最初の呼出だけを処理する 採用!
Slide 32
Slide 32 text
3 TypeScriptでの実装
Slide 33
Slide 33 text
ここまでの話を聞いてどうでしたか? 脳内メモリいっぱいになりましたよね?(私はなった) これら設計上の判断を全て頭に入れてチーム開発、できますか?
Slide 34
Slide 34 text
先程の課題たち イベント発行の信頼性担保 メッセージの重複処理対応 呼び出し関係制御 トレーサビリティ確保 の、他にも… エラー時の補償処理設計 Sagaの複数化/並列実行/呼出順の入れ替わり インフラ自体が途中で落ちたらどうなる? 互換性のないメッセージの バージョンアップをしたいときはどうする? ...などなど 許容できる不整合範囲を見極め、 総合的な判断の上で様々な対策が必要。 vs 1つ1つの判断をメンバーで共有して 実装で徹底し切る&その後知識を 継承して保守し続けるつらさと直面する 非同期処理の技術的複雑性をどう制御するか
Slide 35
Slide 35 text
非同期処理の技術的複雑性を”型”で制御する TypeScript の強みは、圧倒的な型の表現力! ドメイン知識だけじゃなく、 技術的な仕組みやパターンもモデル化できる。 モデル化できるなら、型で表現できる。 型により実装も強制できる。 仕組みが型レベルで分かれば、 複雑なプロセスでも認知性・予測可能性が上がる。
Slide 36
Slide 36 text
① フローの全体像を『 付の状態マシン+イベント』として表現 型 ② 設計上重要な『EventSourcing』と『Outboxパターン』を で強制 型 ③ 忘れがちなサービスの重複呼出防止の仕組みを で強制 型 ④ イベントペイロードのバージョンアップを 安全に行う 型 今回は 4 つ事例を紹介 非同期処理の技術的複雑性を”型”で制御する
Slide 37
Slide 37 text
コマンドの実行(実線部分) [to 請求書] 請求反映コマンド [to 案件] 請求反映承認/棄却の追跡コマンド ※この棄却追跡が補償トランザクション イベントの検知(点線部分) [from 案件] 運賃確定イベント [from 請求書] 請求書変更承認/棄却イベント [from 案件] 承認/棄却処理完了イベント ① 『フローの全体像』を型で表現 請求連携プロセスを実現するサーガを制御するオーケストレータの処理は 下記のようになる
Slide 38
Slide 38 text
① 『フローの全体像』を型で表現 フローの全体像を『型付状態マシン+イベント』でモデル化 イベントで遷移する状態マシン
Slide 39
Slide 39 text
① 『フローの全体像』を型で表現 イベントで遷移する状態マシン
Slide 40
Slide 40 text
① 『フローの全体像』を型で表現 イベントで遷移する状態マシン 実装を見なくても型だけで全体像がわかる 実装が型レベルで保証される 副次的効果として、状態やイベントを早期に漏れなく洗い出せる
Slide 41
Slide 41 text
今回の構成:コレオグラフィとオーケストレーションの併用 ② 『EventSourcing』と『Outbox』を型で強制
Slide 42
Slide 42 text
コレオグラフィ用とオーケストレーション用、それぞれの Outbox を用意し 全てを同一トランザクションでコミットする ② 『EventSourcing』と『Outbox』を型で強制 Outboxパターン 復習 更新された集約の状態と業務イベントの両方を 一つのトランザクションとしてデータベースに コミットする
Slide 43
Slide 43 text
EventSourcingの強制:Entityの更新はEventをapplyする形で統一 ② 『EventSourcing』と『Outbox』を型で強制 Entityの状態変化はapply(entity, event)経由で 起こす仕組みにする。 「状態を変えるにはイベントを適用するしかない」 という形にしておくと、設計レビューでも 「この更新に対応するイベントは何か?」が 自然に議論されるようになる。
Slide 44
Slide 44 text
Repositoryのインターフェイスで、 `save(entity, event)` 以外の形を許さない 型というよりはシグネチャの制約だが 「違反したコードがそもそも書けない」 ようにする工夫をどう入れるかという目線 Outboxパターンの強制:Repositoryのインターフェースで統合 ② 『EventSourcing』と『Outbox』を型で強制
Slide 45
Slide 45 text
今回はフローの状態管理テーブルを利用し、sagaIdとstatusでのレコードのロックをしたうえで 後続プロセスを起動する、というルールを設けた ※ オーケストレータ含めた全モジュールを同一DB&プロセスに 載せているからできる荒業なので注意 ③ サービス重複呼び出し防止 復習 Idempotency-Keyで最初の呼出だけを処理する ※Idempotency-KeyにはeventId等を利用
Slide 46
Slide 46 text
「ロック済みであることを保証する型」を用いてプロセスを進行させる ③ サービス重複呼び出し防止
Slide 47
Slide 47 text
タグ付きユニオンでversionをタグにしたversioned payload z.discriminatedUnion() を使うことで runtimeでは Zodが実際の値をschemaで検証し コンパイラではversionごとにpayload型が絞り込まれる ペイロードのバージョン管理と検証は zodを組み合わせると便利 ④ イベントペイロードのバージョンアップ →段階的なスキーマ進化を安全に行える
Slide 48
Slide 48 text
まとめ
Slide 49
Slide 49 text
まとめ 非同期連携は奥が深い。 TypeScript×非同期連携 同期処理はシンプルで強力だが、やはり限界がある。タイミングを見て非同期へ。 ただ、どうしても複雑になる。ポイントを抑えて1つずつ対処する必要がある。 複雑、かつ覚えることが多くなりがちな実装も、丁寧にモデリングすれば 型やシグネチャに落としていける。 小さい工夫を随所に積み重ねていくことでコード内に知識がたまり、 バグも少なく見通しがよいコードベースになっていく。 やっぱりTSの型の表現力のポテンシャルはすごい! 色々なパターンをもっと勉強していきたい。
Slide 50
Slide 50 text
シリーズB調達、採用強化中です!!! 物流基盤の根幹を担う アーキテクト 物流のデジタルとAIの最前線を切り拓く モバイルエンジニア 物流産業のデジタル化を加速させる 1人目デザイナー 産業と社会に向き合い続ける プロダクトエンジニア 11月13日 シリーズB調達 シリーズB 特設サイト シリーズB CTO note 未来の物流基盤を共につくる 仲間を切実に求めてます!
Slide 51
Slide 51 text
物流の真価を開き、 あらゆる産業を支える