ドメインモデル設計¶
概要¶
本ドキュメントでは、財務会計システムのドメインモデル設計を定義します。ドメイン駆動設計(DDD)の戦術的設計パターンに従い、エンティティ、値オブジェクト、集約、ドメインサービス、リポジトリを定義します。
1. ドメインモデル全体構造¶
2. 値オブジェクト¶
2.1 金額(Money)¶
金額を表現する値オブジェクト。通貨コードと金額のペアで管理し、加減算などの演算をサポートします。
ビジネスルール: - 金額は null 不可 - 小数点以下2桁で丸め(HALF_UP) - 異なる通貨コード間での演算は不可 - デフォルト通貨は "JPY"
実装例:
値オブジェクトは @Value を使用してイミュータブルに設計します。演算メソッドは新しいインスタンスを返します。
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
/**
* 金額を表現する値オブジェクト
*
* <p>イミュータブル設計により、すべての演算は新しいインスタンスを返す。
* 通貨コードと金額のペアで管理し、異なる通貨間の演算は禁止。</p>
*/
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Money {
public static final Money ZERO = new Money(BigDecimal.ZERO, "JPY");
BigDecimal amount;
String currencyCode;
/**
* ファクトリメソッド - JPY 金額を作成
*/
public static Money of(BigDecimal amount) {
return new Money(
amount.setScale(0, RoundingMode.HALF_UP),
"JPY"
);
}
/**
* ファクトリメソッド - long 値から JPY 金額を作成
*/
public static Money of(long amount) {
return of(BigDecimal.valueOf(amount));
}
/**
* 加算 - 新しい Money インスタンスを返す
*/
public Money add(Money other) {
validateSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currencyCode);
}
/**
* 減算 - 新しい Money インスタンスを返す
*/
public Money subtract(Money other) {
validateSameCurrency(other);
return new Money(this.amount.subtract(other.amount), this.currencyCode);
}
/**
* 乗算 - 新しい Money インスタンスを返す
*/
public Money multiply(int multiplier) {
return new Money(
this.amount.multiply(BigDecimal.valueOf(multiplier)),
this.currencyCode
);
}
public boolean isZero() {
return amount.compareTo(BigDecimal.ZERO) == 0;
}
public boolean isPositive() {
return amount.compareTo(BigDecimal.ZERO) > 0;
}
public boolean isNegative() {
return amount.compareTo(BigDecimal.ZERO) < 0;
}
public Money abs() {
return isNegative() ? new Money(amount.abs(), currencyCode) : this;
}
private void validateSameCurrency(Money other) {
if (!this.currencyCode.equals(other.currencyCode)) {
throw new IllegalArgumentException(
"異なる通貨間の演算はできません: " +
this.currencyCode + " vs " + other.currencyCode
);
}
}
}
2.2 勘定科目コード(AccountCode)¶
勘定科目コードを表現する値オブジェクト。4桁の数字で構成され、先頭2桁で科目カテゴリを判定できます。
ビジネスルール: - 4桁の数字のみ許可 - 先頭2桁でカテゴリを判定: - 11-19: 資産 - 21-29: 負債 - 31-39: 純資産 - 41-49: 収益 - 51-79: 費用
2.3 仕訳伝票番号(JournalEntryNumber)¶
仕訳伝票番号を表現する値オブジェクト。"JE" + 日付(yyyyMMdd)+ "-" + 連番(4桁)の形式で採番されます。
ビジネスルール:
- 形式: JEyyyyMMdd-0001 (例: JE20250115-0001)
- 同一日内で連番を採番
- 一度発行された番号は変更不可
2.4 貸借区分(DebitCreditType)¶
借方・貸方を表現する列挙型。
2.5 仕訳ステータス(JournalStatus)¶
仕訳の状態を表現する列挙型。
状態遷移:
2.6 勘定科目種別(AccountType)¶
勘定科目の5分類を表現する列挙型。
ビジネスルール: - B/S(貸借対照表): 資産、負債、純資産 - P/L(損益計算書): 収益、費用 - 借方残高: 資産、費用 - 貸方残高: 負債、純資産、収益
3. エンティティ¶
3.1 勘定科目(Account)¶
勘定科目マスタのエンティティ。@Value と @With を使用してイミュータブルに設計します。
設計方針:
| アノテーション | 用途 |
|---|---|
@Value |
すべてのフィールドを private final にし、Getter、equals/hashCode、toString を自動生成 |
@With |
フィールドを変更した新しいインスタンスを生成するメソッドを自動生成 |
@AllArgsConstructor(access = AccessLevel.PRIVATE) |
コンストラクタをプライベートにしてファクトリメソッド経由のみで生成 |
生成法:
| メソッド | 用途 | 説明 |
|---|---|---|
create() |
新規作成 | ビジネスルールに従って新しいエンティティを生成。ID 自動生成、バリデーション実行 |
reconstruct() |
再構築 | DB からの復元用。バリデーションをスキップし、既存データをそのまま復元 |
withXxx() |
更新 | @With により自動生成。変更したいフィールドのみ指定して新しいインスタンスを返す |
3.2 勘定科目構成(AccountStructure)¶
勘定科目の階層構造を管理するエンティティ。チルダ連結方式で階層パスを管理します。
チルダ連結方式:
階層パス = "11~11000~11190~11110"
(勘定分類~大分類~中分類~小分類)
3.3 課税取引(TaxableTransaction)¶
消費税の課税取引マスタエンティティ。
4. 集約¶
4.1 仕訳集約(JournalEntry Aggregate)¶
仕訳は3層構造(仕訳→仕訳明細→仕訳貸借明細)で構成される集約です。すべてのエンティティは @Value と @With を使用してイミュータブルに設計します。
不変条件(Invariant): 1. 借方合計 = 貸方合計(複式簿記の原則) 2. 仕訳明細は2件以上必要 3. 借方と貸方の両方が必要 4. 金額は0より大きい正の値
ファクトリメソッド:
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.With;
/**
* 仕訳エンティティ(集約ルート)
*
* <p>イミュータブル設計により、状態変更は常に新しいインスタンスを返す。
* 不変条件(貸借一致など)は create() で保証する。</p>
*/
@Value
@With
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class JournalEntry {
JournalEntryNumber journalNumber;
LocalDate journalDate;
String description;
JournalStatus status;
List<JournalEntryDetail> details;
int version;
/**
* ファクトリメソッド - 新規作成
*
* <p>不変条件を検証し、新しい仕訳を作成する。</p>
*/
public static JournalEntry create(
LocalDate journalDate,
String description,
List<JournalEntryDetail> details) {
// 不変条件1: 仕訳明細は必須
if (details == null || details.isEmpty()) {
throw new IllegalArgumentException("仕訳明細が必要です");
}
// 不変条件2: 借方と貸方の両方が必要
boolean hasDebit = details.stream().anyMatch(JournalEntryDetail::isDebit);
boolean hasCredit = details.stream().anyMatch(JournalEntryDetail::isCredit);
if (!hasDebit || !hasCredit) {
throw new IllegalArgumentException("借方と貸方の両方が必要です");
}
// 不変条件3: 貸借一致
Money totalDebit = calculateTotalDebit(details);
Money totalCredit = calculateTotalCredit(details);
if (!totalDebit.equals(totalCredit)) {
throw new IllegalArgumentException(
String.format("借方と貸方が一致しません: 借方=%s, 貸方=%s",
totalDebit, totalCredit));
}
return new JournalEntry(
JournalEntryNumber.generate(journalDate),
journalDate,
description,
JournalStatus.DRAFT,
List.copyOf(details), // イミュータブルリストにコピー
0 // 新規作成時は version = 0
);
}
/**
* 再構築用ファクトリメソッド - DB からの復元
*/
public static JournalEntry reconstruct(
JournalEntryNumber journalNumber,
LocalDate journalDate,
String description,
JournalStatus status,
List<JournalEntryDetail> details,
int version) {
return new JournalEntry(journalNumber, journalDate, description,
status, List.copyOf(details), version);
}
// ===== 状態変更メソッド(新しいインスタンスを返す) =====
/**
* 承認する - 承認済ステータスの新しい仕訳を返す
*/
public JournalEntry approve() {
if (!status.canApprove()) {
throw new IllegalStateException("この仕訳は承認できません: " + status);
}
return this.withStatus(JournalStatus.APPROVED);
}
/**
* 確定する - 確定済ステータスの新しい仕訳を返す
*/
public JournalEntry confirm() {
if (!status.canConfirm()) {
throw new IllegalStateException("この仕訳は確定できません: " + status);
}
return this.withStatus(JournalStatus.CONFIRMED);
}
/**
* 差し戻す - 差戻しステータスの新しい仕訳を返す
*/
public JournalEntry reject() {
return this.withStatus(JournalStatus.REJECTED);
}
/**
* 取り消す - 取消済ステータスの新しい仕訳を返す
*/
public JournalEntry cancel() {
return this.withStatus(JournalStatus.CANCELLED);
}
}
4.2 自動仕訳パターン集約¶
自動仕訳の設定パターンを管理する集約。@Value と @With を使用してイミュータブルに設計します。
5. ドメインサービス¶
5.1 仕訳ドメインサービス¶
複数の仕訳にまたがるビジネスロジックを実装するドメインサービス。
検証ルールパターン:
public interface JournalValidationRule {
boolean isSatisfiedBy(JournalEntry entry);
String getErrorMessage();
}
// 貸借バランスルール
class BalanceRule implements JournalValidationRule {
@Override
public boolean isSatisfiedBy(JournalEntry entry) {
return entry.isBalanced();
}
@Override
public String getErrorMessage() {
return "借方と貸方が一致しません";
}
}
5.2 残高計算ドメインサービス¶
勘定科目残高の計算を行うドメインサービス。
5.3 財務分析ドメインサービス¶
財務諸表の生成と財務指標の計算を行うドメインサービス。
6. リポジトリ¶
6.1 勘定科目リポジトリ¶
6.2 仕訳リポジトリ¶
6.3 日次勘定科目残高リポジトリ¶
6.4 月次勘定科目残高リポジトリ¶
7. 財務諸表ドメインモデル¶
7.1 貸借対照表(BalanceSheet)¶
貸借対照表は @Value を使用してイミュータブルに設計します。
貸借平均の原則:
資産 = 負債 + 純資産
7.2 損益計算書(IncomeStatement)¶
損益計算書は @Value を使用してイミュータブルに設計します。
利益計算:
売上総利益 = 売上高 - 売上原価
営業利益 = 売上総利益 - 販売費及び一般管理費
経常利益 = 営業利益 + 営業外収益 - 営業外費用
当期純利益 = 経常利益 + 特別利益 - 特別損失 - 法人税等
7.3 財務指標(FinancialRatios)¶
財務指標は値オブジェクトとして @Value を使用してイミュータブルに設計します。
主要財務指標の計算式:
| 指標 | 計算式 |
|---|---|
| 売上高総利益率 | 売上総利益 ÷ 売上高 × 100 |
| 売上高営業利益率 | 営業利益 ÷ 売上高 × 100 |
| ROA(総資本利益率) | 当期純利益 ÷ 総資産 × 100 |
| ROE(自己資本利益率) | 当期純利益 ÷ 自己資本 × 100 |
| 流動比率 | 流動資産 ÷ 流動負債 × 100 |
| 当座比率 | 当座資産 ÷ 流動負債 × 100 |
| 自己資本比率 | 自己資本 ÷ 総資本 × 100 |
| 総資本回転率 | 売上高 ÷ 総資本 |
8. アプリケーションサービス¶
8.1 勘定科目サービス¶
8.2 仕訳サービス¶
トランザクション管理:
@Service
@RequiredArgsConstructor
@Transactional
public class JournalEntryService {
public JournalEntry createJournalEntry(CreateJournalEntryCommand command) {
// 1. ビジネスルール検証
validateJournalEntry(command);
// 2. 仕訳を保存
JournalEntry saved = journalRepository.save(command.toJournalEntry());
// 3. 日次残高を更新
updateDailyBalances(saved);
return saved;
}
}
8.3 財務諸表サービス¶
9. パッケージ構成¶
com.example.accounting/
├── domain/
│ ├── model/
│ │ ├── account/
│ │ │ ├── Account.java # 勘定科目エンティティ
│ │ │ ├── AccountType.java # 勘定科目種別(列挙型)
│ │ │ └── AccountCode.java # 勘定科目コード(値オブジェクト)
│ │ ├── journal/
│ │ │ ├── JournalEntry.java # 仕訳(集約ルート)
│ │ │ ├── JournalEntryDetail.java # 仕訳明細
│ │ │ ├── JournalEntryDetailItem.java # 仕訳貸借明細
│ │ │ ├── JournalEntryNumber.java # 仕訳伝票番号(値オブジェクト)
│ │ │ ├── JournalStatus.java # 仕訳ステータス(列挙型)
│ │ │ ├── DebitCreditType.java # 貸借区分(列挙型)
│ │ │ └── Money.java # 金額(値オブジェクト)
│ │ ├── balance/
│ │ │ ├── DailyBalance.java # 日次勘定科目残高
│ │ │ └── MonthlyBalance.java # 月次勘定科目残高
│ │ └── financial/
│ │ ├── BalanceSheet.java # 貸借対照表
│ │ ├── BalanceSheetItem.java # 貸借対照表項目
│ │ ├── IncomeStatement.java # 損益計算書
│ │ ├── IncomeStatementItem.java # 損益計算書項目
│ │ ├── FinancialRatios.java # 財務指標
│ │ └── AccountCategory.java # 勘定科目区分(列挙型)
│ └── service/
│ ├── JournalDomainService.java # 仕訳ドメインサービス
│ ├── BalanceCalculationService.java # 残高計算ドメインサービス
│ └── FinancialAnalysisDomainService.java # 財務分析ドメインサービス
│
├── application/
│ ├── port/
│ │ ├── in/
│ │ │ ├── AccountUseCase.java # 勘定科目ユースケース
│ │ │ ├── JournalEntryUseCase.java # 仕訳ユースケース
│ │ │ ├── FinancialStatementUseCase.java # 財務諸表ユースケース
│ │ │ ├── command/
│ │ │ │ ├── CreateAccountCommand.java
│ │ │ │ ├── UpdateAccountCommand.java
│ │ │ │ └── CreateJournalEntryCommand.java
│ │ │ └── query/
│ │ │ ├── BalanceSheetQuery.java
│ │ │ └── IncomeStatementQuery.java
│ │ └── out/
│ │ ├── AccountRepository.java # 勘定科目リポジトリ
│ │ ├── JournalEntryRepository.java # 仕訳リポジトリ
│ │ ├── DailyBalanceRepository.java # 日次残高リポジトリ
│ │ └── MonthlyBalanceRepository.java # 月次残高リポジトリ
│ └── service/
│ ├── AccountService.java # 勘定科目サービス
│ ├── JournalEntryService.java # 仕訳サービス
│ └── FinancialStatementService.java # 財務諸表サービス
│
└── infrastructure/
├── persistence/
│ ├── entity/
│ │ ├── AccountEntity.java
│ │ ├── JournalEntryEntity.java
│ │ └── ...
│ ├── mapper/
│ │ ├── AccountMapper.java
│ │ ├── JournalEntryMapper.java
│ │ └── ...
│ └── repository/
│ ├── AccountRepositoryImpl.java
│ ├── JournalEntryRepositoryImpl.java
│ └── ...
└── web/
├── controller/
│ ├── AccountController.java
│ ├── JournalEntryController.java
│ └── FinancialStatementController.java
└── dto/
├── AccountRequest.java
├── AccountResponse.java
├── JournalEntryRequest.java
└── JournalEntryResponse.java
10. 設計原則¶
10.1 リッチドメインモデル¶
ビジネスロジックはドメインモデル(エンティティ、値オブジェクト)に集約します。
| 配置場所 | 責務 |
|---|---|
| エンティティ | 単一エンティティの操作、自身の状態に基づく計算、自身の整合性チェック |
| ドメインサービス | 複数エンティティにまたがる操作、外部情報が必要な計算、複数エンティティの整合性チェック |
| アプリケーションサービス | トランザクション管理、ユースケースの調整、リポジトリ呼び出し |
10.2 イミュータブル設計¶
すべてのドメインオブジェクト(エンティティ、値オブジェクト)は @Value と @With を使用してイミュータブルに設計します。
使用するアノテーション:
| アノテーション | 用途 |
|---|---|
@Value |
すべてのフィールドを private final にし、Getter、equals/hashCode、toString を自動生成 |
@With |
フィールドを変更した新しいインスタンスを生成するメソッドを自動生成 |
@AllArgsConstructor(access = AccessLevel.PRIVATE) |
コンストラクタをプライベートにしてファクトリメソッド経由のみで生成 |
生成法(ファクトリメソッド):
| メソッド | 用途 | バリデーション |
|---|---|---|
create() |
新規作成 | 実行する |
reconstruct() |
DB からの復元 | スキップ |
withXxx() |
フィールド変更 | 必要に応じてラップ |
イミュータブル設計のメリット: - スレッドセーフ(並行処理で安全) - 副作用がない(予測可能な動作) - テストしやすい - 変更履歴の追跡が容易
10.3 不変条件の保証¶
コンストラクタおよびファクトリメソッドで不変条件を保証します。
@Value
@With
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class JournalEntry {
// ... フィールド定義 ...
public static JournalEntry create(...) {
// 不変条件の検証
if (!isBalanced(details)) {
throw new IllegalArgumentException("借方と貸方が一致しません");
}
return new JournalEntry(...);
}
// 状態変更は新しいインスタンスを返す
public JournalEntry approve() {
if (!status.canApprove()) {
throw new IllegalStateException("承認できません");
}
return this.withStatus(JournalStatus.APPROVED);
}
}
10.4 値オブジェクトの活用¶
プリミティブ型の代わりに値オブジェクトを使用し、型安全性とドメイン知識の表現を向上させます。
| プリミティブ | 値オブジェクト | メリット |
|---|---|---|
| String | AccountCode | 4桁数字のバリデーション、カテゴリ判定 |
| BigDecimal | Money | 通貨コード管理、丸め処理、演算 |
| String | JournalEntryNumber | 形式バリデーション、日付・連番抽出 |
10.5 依存性逆転の原則¶
リポジトリインターフェースは Application 層に配置し、実装は Infrastructure 層に配置します。
Domain Layer ← Application Layer → Infrastructure Layer
↑ ↑ ↓
└──────────────┴────────────────────┘
依存方向: Infrastructure → Application → Domain