Upgrade to Pro — share decks privately, control downloads, hide ads and more …

製造業の会計システムをDDDで開発した話

 製造業の会計システムをDDDで開発した話

概要
本資料は2025年3月27日開催の「ドメイン駆動設計 - 実践企業が語るBefore/After - Vol.2」のイベント登壇資料です。
エンジニア採用情報はこちら
https://recruit.caddi.tech/

[email protected]

March 26, 2025
Tweet

Other Decks in Technology

Transcript

  1. 自己紹介 • Yo Matsumoto ◦ 兵庫県在住 ◦ 基本リモートワークですが、月に 1〜2回ほど東京のオフィスに行っています ◦

    趣味:スノーボード、ボードゲーム、猫 2匹 • キャディ株式会社では ◦ 生産管理システムのバックエンドエンジニアを 1年 ◦ 図面データ活用SaaSのバックエンドエンジニアを 1年6ヶ月 2
  2. © CADDi Inc. Mission モノづくり産業のポテンシャルを解放する Unleash the potential of manufacturing

    モノづくりに携わるすべての⼈が、 本来持っている⼒を最⼤限に発揮できる社会を実現する。 そのために私たちは、産業の常識を変える「新たな仕組み」をつくります。 現在モノづくり産業では、⾮常に多くの⼒が埋もれたままになっています。 ⾒積業務や管理業務に忙殺される、 営業⼒が⾜りない、情報やネットワークが乏しい。 あらゆる理由によってがんじがらめにされ、 本来の開発⼒や技術⼒を発揮しきれていません。 こうした縛りをほどくことで、各企業のポテンシャルを解放。 産業全体に⼤きな⼒を⽣み出し、豊かにすることが私たちの使命です。 ⼩さな町⼯場も、歴史ある⼤規模メーカーも、創⽴まもないベンチャーも。 すべてのモノづくり企業が強みを活かして輝き、新たな価値がたくさん⽣まれる。 そんな未来を切り拓くために、私たちは挑み続けます。 3
  3. • 背景 ◦ 約 2 年前に構築した社内向けプロダクトで現在はサービス終了している ▪ 当時は受発注プラットフォームを運営しものづくりも行っていた ▪ なお、現在はナレッジを集約し

    製造業AIデータプラットフォーム を構築している ◦ 当時、ビジネスが拡大し扱う製品、在庫の量や種類が増え、会計オペレーションの複雑さが限界を迎えつつ あった • 目的 ◦ 拡大する会計オペレーションを持続可能かつ再現可能にする • システム概要 ◦ バッチ処理として最低月に1回稼働し、締めた会計仕訳を保存する ◦ 会計仕訳は、受発注や在庫管理を行う上流システムのイベントから生成される ◦ 会計仕訳の正当性について検証レポートを生成する ◦ 生成された会計仕訳は全社会計基盤にそのまま Importされる 会計システムとは 4
  4. • ドメイン駆動設計のエッセンス エリック・エヴァンスのドメイン駆動設計: ソフトウェアの核心にある複雑さに立ち向かう ◦ コアドメインに集中する ◦ 明示的な境界づけられたコンテキストの内部で、ユビキタス言語を語る ◦ ドメインの実践者とソフトウェアの実践者による創造的な共同作業を通じて、モデルを探求

    • 会計システムでは ◦ コアドメイン : 上流システムのイベントを会計仕訳に変換する部分 ◦ ドメインの実践者 : 経理担当者 • プロジェクト開始時の状況 ◦ 会計仕訳への変換処理の要件は決まっていない ◦ 事業のゴールから逆算した開発期間は 6ヶ月 DDDの利用について考慮したこと 6
  5. • キャディ では DDD が普通に利用されており、共通言語となっていた ◦ 生産管理システムは開発済みで、 DDD を採用して開発 ◦

    より実践的な、 実践ドメイン駆動設計 や 関数型ドメインモデリング といった書籍も普及 ◦ 新規システムは当然 DDD でやるよね、という雰囲気は醸成されていた • しかし、DDD に馴染みがない中途採用者もいる ◦ 育成期間はある程度かかる ◦ 後でチームが拡大することがわかっており、 他のシステムと構造や考え方が揃っていることによるメリットが 上回ると判断した DDDの利用について考慮したこと 7
  6. • システムの状況とDDD ◦ イベントの種類や仕訳の種類で分類する ◦ 要件が定まっていないことから、徐々に実施範囲を広 げながらドメインを洗練させる方針 • バッチ処理とDDD ◦

    バッチ処理はUIがない = 検証ロジックやエラーハンド リングがない ◦ ドメインロジックがクリーンに保ちやすいのではという 仮説があった • 会計処理とDDD ◦ 会計処理はかなり純粋な計算ロジックになる ◦ ドメインをカプセル化して Testable にしやすい DDDの利用について考慮したこと 8 実施範囲を決める図
  7. ドメインの用語を整理する • ユビキタス辞書 ◦ システム固有の概念を整理する ◦ ググればよいことは書かない • 会計用語集 ◦

    金融庁の EDINETタクソノミ が公開している 勘定科目リストを利用 ◦ https://www.fsa.go.jp/search/20241112.html ドメインモデリングを合意する 12
  8. • DFDの利用 ◦ 上流のイベントと バウンディッドコン テキスト内の仕訳の変換が複雑 ◦ どのように変換できるかを把握できる ようにした •

    運用フェーズで利用 ◦ 経理部門でも長く利用する資料に なったとのこと ドメインモデリングを合意する 14
  9. • 1例として、Repository は… ◦ 依存関係逆転の原則 (DIP)を使う ▪ trait(Rust における interface

    のようなもの) は domain 層に配置 ▪ 実装は infrastructure層に配置 ◦ mockall crateを使ってmockを自動実装させて usecase 層のテストを書きやすく ◦ 具体的なドメイン実装の紹介 17 // domain/accounting/src/repository/accounting_journal_repository.rs #[cfg_attr(any(test, feature = "mock"), mockall::automock)] #[async_trait::async_trait] pub trait AccountingJournalRepository { async fn find_by_id (&self, id: &JournalId) -> anyhow:: Result<AccountingJournal>; async fn insert(&self, accounting_journal: AccountingJournal) -> anyhow:: Result<()>; } // infra/repository_impl/repository_impl_pg/src/repository_impl_pg/accounting_journal_repository_pg.rs #[async_trait::async_trait] impl AccountingJournalRepository for AccountingJournalRepositoryPg { async fn find_by_id (&self, id: &JournalId) -> anyhow:: Result<AccountingJournal> { // ... } async fn insert(&self, accounting_journal: AccountingJournal) -> anyhow:: Result<()> { // ... } }
  10. • 意味の違う数値を異なる型として表現 (Value Object) ◦ 総額(TotalAmount)、単価(UnitAmount)、在庫数量(InventryAmount)... etc ◦ ソースコードにドメインの計算ロジックを直接記述できる ◦

    誤って異なる型を使用した場合、ビルド時にエラーになる ◦ コード修正の際の矛盾 = ビジネスロジックの矛盾 となり活発な議論を醸成する下地に 具体的なドメイン実装の紹介 18 総額 = 単価 × 在庫数量の計算 pub struct AccountingAmount <T> { value: Decimal, amount_type: PhantomData<T>, } pub type TotalAmount = AccountingAmount<amount_type::Total>; pub type UnitAmount = AccountingAmount<amount_type::Unit>; pub type InventoryQuantity = TaggedQuantity<quantity_type::Inventory>; // pub type quantity_type::Inventory impl UnitAmount { pub fn multiply_with (&self, quantity: InventoryQuantity) -> anyhow:: Result<TotalAmount> { // ... } }
  11. • 集約の単位を定義する (Aggregate) ◦ Aggregate とは ▪ Repository の読み込み/保存の単位で、1 Transaction

    になる ▪ 1つの Aggregate Root を持ち、 Aggregate をまたがるときは id で参照を保持する ▪ 一般的には大きな AggregateRoot は NGとされるが… ◦ このシステムでは最大 1ヶ月分の仕訳を含む AggregateRoot: AccountingJournal を定義した ▪ 1回のバッチの処理単位が最大 1ヶ月分であるため構造がシンプルに ▪ Webアプリではなかったためレスポンスが遅い、などの問題はなかった 具体的なドメイン実装の紹介 19 // domain/accounting/src/aggregate/accounting_journal.rs pub struct AccountingJournal { id: JournalId, transactions: Vec<AccountingTransaction>, } // domain/accounting/src/aggregate/accounting_journal/accounting_transaction.rs pub struct AccountingTransaction { id: AccountingTransactionId, accounting_date: AccountingDate, entries: AccountingEntrySet, }
  12. • バッチ処理の上位構造はいわゆるワークフローになっている 具体的なドメイン実装の紹介 20 pub struct CreationSetInitialized { id: JournalCreationSetId,

    } pub struct CreationSetInventoryCreated { id: JournalCreationSetId, inventory_id: InventoryId, } pub struct CreationSetJournalCreated { id: JournalCreationSetId, inventory_id: InventoryId, journal_id: JournalId, } pub struct CreationSetReportCreated { id: JournalCreationSetId, inventory_id: InventoryId, journal_id: JournalId, report_id: ReportId, } async fn create_journal ( &self, creation_set: CreationSetInventoryCreated, ) -> anyhow:: Result<CreationSetJournalCreated> { // ... } • Workflow パターンを利用 ◦ 状態そのものを Entity にする ◦ ある状態を受け取って次の状態を返す関数でシ ステムを構築する ◦ 関数型ドメインモデリング に 記述されているパターン
  13. 上流イベントの結果とデータ上の在庫数の一致をレポート化 • 前月末在庫数 + 今月入庫数 - 今月出庫数 = 今月末在庫数 •

    月次締め前に複数回チェックし異常を検知、修正する 具体的なドメイン実装の紹介 21
  14. • 後で参加したメンバーのスムーズなキャッチアップ ◦ ビジセスと実装の乖離が少ない ◦ ドメインモデル、ユビキタス辞書などの資料 • シンプルなビジネスロジックによる少ない実装起因バグ ◦ ドメイン内のコードをユビキタス言語で表現する

    ◦ バウンデッドコンテキストを用いた会計ドメインの保護 ◦ 運用フェーズで、実装に起因するバグは数件件程度の発生 • ビジネス上の制約を加味した開発スコープの適切なコントロール ◦ 当初の開発期間は6ヶ月だったが、最終的に開発期間は 9ヶ月になった ◦ 監査法人との会計処理確定タイミングに従って、 6ヶ月で必要十分な機能リリースができた DDDの実践で得られた効果 23