はじめに
こんにちは!
エンジニアの matsuura です。
みなさん、ソフトウェア開発していますか?
昨今、ソフトウェア開発において、ビジネス要件の複雑化と技術スタックの多様化が進んでいます。
このような環境では、コアビジネスロジックを技術的な詳細から守ることが、持続可能な開発を実現する鍵となります。
例えば、ECサイトで「在庫がある商品のみ注文可能」というビジネスルールがあるとします。
このルールが、「コアビジネスロジック」に該当します。
ここで、もし技術的な変化、例えば、データベースがMySQLからPostgreSQLに変わったり、フロントエンドがReactからVueに変わったりしたとしても、本質的には変わらないはずです。
今回紹介するオニオンアーキテクチャは、この「本質的なビジネスロジック」を中心に据え、依存関係逆転の原理で技術的な詳細から守るアーキテクチャパターンです。
オニオンアーキテクチャについて
従来のレイヤードアーキテクチャの罠
従来のレイヤードアーキテクチャでは、上位層が下位層に依存する「溝落ち型」の構造を持っています。
この構造の「依存関係」とは、上位層が下位層の具体的な実装詳細に結びついている状態です。
例えば、Service層が「user_name」や「user_id」といったDBカラム名を直接知っているような状態です。
このおかげで、下層の変更が「ドミノ倒し」のように上層まで波及してしまう問題を有しているのです。
で、何が問題なのか?
実際にありそうなシナリオを例にレイヤードアーキテクチャの問題点を解説します。
シナリオ1: データベースの技術変更
プロジェクトの途中で、パフォーマンスやコストの理由でMySQLからPostgreSQL、さらにはMongoDBへの移行が決定しました。
◯従来のアーキテクチャでは…
- テーブル設計の大幅変更
- EntityクラスのSQL固有の気遣いを修正
- Service層のEntity依存部分を全て書き直し
- ビジネスロジックのテストを全てやり直し
シナリオ2: 外部APIのサービス変更
決済サービスをStripeからPayPalに変更したいのですが、APIのResponse形式が全く違う。
◯従来のアーキテクチャでは…
- 決済処理のコアロジックが外部APIのデータ構造に依存
- API変更でビジネスルールまで影響を受ける
オニオンアーキテクチャは、こうした「技術の波及」を防ぐための防波堤を構築します。
オニオンアーキテクチャとは
依存関係逆転の原理
オニオンアーキテクチャの核心は、DomainとInfrastructureの依存関係を逆転させることです!
え!?それだけ?
それだけです。
レイヤードアーキテクチャとの違いをより具体的に列挙するならば、以下の通りとなります。
- Domain層で、Repositoryインターフェースを定義する
- Infrastructure内で、その実装クラスを定義する
- DomainはDBに依存しない設計にする
この設計により、DBの変更があってもDomain層は影響を受けません。
ビジネスを中心に据えた「防波堤」の作り方
Infrastructure層の最大の使命は「技術的な詳細を完全に防波堤の向こうに閉じ込める」ことです。
具体的な防波方法:
- Infrastructure層において、
user_nm
やcreated_at
といったDB固有の命名を他の層に漏らさない - DomainModelでは
user_nm
というDB固有の命名を使用せず、User
クラスのname
プロパティとして表現する - Infrastructure層が「翻訳器」となり、DB用語とビジネス用語を相互変換する
- 例) DBの
user_nm
カラム ⇔ DomainModelのUser.name
プロパティ
- 例) DBの
こうすることで、DBが変わってもビジネスロジック(Domain層)は1行も変更しなくて済む理想的な状態を作り出せます。
各層の詳細説明
Domain Model層
アプリケーションのビジネスドメインを表現する層です。
- 特徴
- モノ・ヒト・コトに分類されるビジネス概念を表現
- データベースのテーブル定義に依存しない
- Entityはあくまでテーブル定義であり、Domain Modelとは別物
Domain Service層
ビジネスロジックを記述する層です。
- 特徴
- ビジネスルールの実装
- Repositoryインターフェースの定義
- ドメイン固有の処理ロジック
Application Service層
ユーザーの行動(ユースケース)を表現する層です。
- 特徴
- 各ユースケースごとにクラスを作成
- Domain Serviceのインターフェースを順次呼び出し
- ビジネスロジックは含まず、オーケストレーションに専念
Infrastructure層
外部システムとの連携を担当する層です。
- 特徴
- Repositoryの実装クラス
- 外部API、データベースへのアクセス
- 外部システムの影響を他層に波及させない
Presentation層
APIのリクエスト・レスポンスに責任を持つ層です。
- 特徴
- HTTPリクエストを受け付る
- レスポンスの生成する
- 入力値の検証とDomainModelへの変換する
実装例とディレクトリ構成
ECサイトの商品管理でのRepository実装例
// Domain Model層: 商品の情報を表現
public class Product {
private final ProductId id;
private final ProductName name;
private final Price price;
private final Stock stock;
public Product(ProductId id, ProductName name, Price price, Stock stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
// ビジネスロジック: 在庫チェック
public boolean isAvailable(int requestedQuantity) {
return stock.getValue() >= requestedQuantity;
}
// (省略)
}
// Value Object: 商品ID
public class ProductId {
private final String value;
public ProductId(String value) {
if (value == null || value.isEmpty()) {
throw new IllegalArgumentException("Product ID cannot be empty");
}
this.value = value;
}
public String getValue() { return value; }
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (!(object instanceof ProductId)) return false;
ProductId productId = (ProductId) object;
return Objects.equals(value, productId.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
// Domain Service層: Repositoryインターフェース
public interface ProductRepository {
/**
* 商品IDで商品を検索
* プリミティブ型ではなくValue Objectを使用
*/
Optional<Product> findById(ProductId productId);
/**
* カテゴリ別商品検索
* ページングも考慮した設計
*/
List<Product> findByCategory(Category category, Pageable pageable);
/**
* 商品の保存
* 新規登録・更新を区別しない
*/
void save(Product product);
/**
* 在庫が少ない商品を取得
* ビジネスルールを含んだクエリ
*/
List<Product> findLowStockProducts(int threshold);
}
Infrastructure層でのデータ変換の実装例
// Infrastructure層: DB固有の型からドメインモデルへの変換
@Repository
public class JpaProductRepository implements ProductRepository {
@Autowired
private ProductJpaRepository jpaRepository;
@Override
public Optional<Product> findById(ProductId productId) {
// 1. DB固有の型でクエリ実行
Optional<ProductEntity> entity = jpaRepository.findById(productId.getValue());
// 2. DBエンティティをドメインモデルに変換
return entity.map(this::toDomainModel);
}
@Override
public void save(Product product) {
// 1. ドメインモデルをDBエンティティに変換
ProductEntity entity = toEntity(product);
// 2. DB固有の型で保存
jpaRepository.save(entity);
}
// ドメインモデル → DBエンティティ変換
private ProductEntity toEntity(Product product) {
ProductEntity entity = new ProductEntity();
entity.setId(product.getId().getValue());
entity.setName(product.getName().getValue());
entity.setPrice(product.getPrice().getAmount());
entity.setStock(product.getStock().getValue());
return entity;
}
// DBエンティティ → ドメインモデル変換
private Product toDomainModel(ProductEntity entity) {
return new Product(
new ProductId(entity.getId()),
new ProductName(entity.getName()),
new Price(entity.getPrice()),
new Stock(entity.getStock())
);
}
}
このようにすることで、Infrastructure層がDBの詳細を完全に隠蔽し、他の層にはドメインモデルのみを公開することで、データベース変更の影響を局所化できます。
ECサイトでのプロジェクト構造例
┳ app
┃ ┣ presentation # UI層・API層
┃ ┃ ┣ controllers # RESTコントローラー
┃ ┃ ┃ ┣ ProductController.java
┃ ┃ ┃ ┗ OrderController.java
┃ ┃ ┣ dto # データ転送オブジェクト
┃ ┃ ┃ ┣ ProductResponse.java
┃ ┃ ┃ ┗ CreateOrderRequest.java
┃ ┃ ┗ handlers # エラーハンドリング
┃ ┣ application # アプリケーション層
┃ ┃ ┗ usecase # ユースケース実装
┃ ┃ ┣ ProductApplicationUseCase.java
┃ ┃ ┗ OrderApplicationUseCase.java
┃ ┣ domain # ドメイン層(コア)
┃ ┃ ┣ models # エンティティ・集約
┃ ┃ ┃ ┣ product # 商品集約
┃ ┃ ┃ ┃ ┣ Product.java
┃ ┃ ┃ ┃ ┣ ProductId.java (Value Object)
┃ ┃ ┃ ┃ ┣ ProductName.java
┃ ┃ ┃ ┃ ┣ Price.java
┃ ┃ ┃ ┃ ┗ Stock.java
┃ ┃ ┃ ┣ order # 注文集約
┃ ┃ ┃ ┃ ┣ Order.java
┃ ┃ ┃ ┃ ┣ OrderId.java
┃ ┃ ┃ ┃ ┣ OrderItem.java
┃ ┃ ┃ ┃ ┗ OrderStatus.java
┃ ┃ ┃ ┗ customer # 顧客集約
┃ ┃ ┃ ┣ Customer.java
┃ ┃ ┃ ┗ CustomerId.java
┃ ┃ ┣ services # ドメインサービス
┃ ┃ ┃ ┣ PricingService.java
┃ ┃ ┃ ┗ InventoryService.java
┃ ┃ ┗ repositories # リポジトリインターフェース
┃ ┃ ┣ ProductRepository.java
┃ ┃ ┣ OrderRepository.java
┃ ┃ ┗ CustomerRepository.java
┃ ┗ infrastructure # インフラストラクチャ層
┃ ┣ entities # DBエンティティ(テーブル定義)
┃ ┃ ┣ ProductEntity.java
┃ ┃ ┗ OrderEntity.java
┃ ┣ repositories # Repository実装
┃ ┃ ┣ ProductRepositoryImpl.java
┃ ┃ ┣ OrderRepositoryImpl.java
┃ ┃ ┗ CustomerRepositoryImpl.java
┃ ┣ mappers # ドメインモデル⇔DBエンティティ変換
┃ ┃ ┣ ProductMapper.java
┃ ┃ ┗ OrderMapper.java
┃ ┗ external # 外部API連携
┃ ┣ payment # 決済サービス
┃ ┗ notification # 通知サービス
おまけ
※ この項目はClaude(Sonnet 4)により生成してもらった項目になります。
オニオンアーキテクチャのメリット
- 高い保守性: データベースの変更がビジネスロジックに影響しない
- テスタビリティ: インターフェースによりモックが容易
- 関心の分離: 各層の責務が明確
- 拡張性: 新しい外部システムの追加が容易
導入時のハードルと対策
◯1. チームの学習コスト
- 初回は「なぜこんなに面倒なんだ?」という反応が一般的
- 対策: 小さな機能から始めて、メリットを体感してもらう
◯2. 初期のオーバーエンジニアリングリスク
- 3ヶ月で終わるプロトタイプにはオーバースペック
- 判断基準: 「1年以上運用」「3人以上の開発者」が目安
◯3. レイヤー構造の認知負荷
- 最初は「どのファイルを修正すればいい?」が分からない
- 対策: 明確なディレクトリ構造とネーミング規約で解決
オニオンアーキテクチャを「使うべきか?」の判断フローチャート
以下の質問に「YES」が2個以上あるなら、オニオンアーキテクチャの導入を検討してみてください:
- ✅ 「1年以上継続運用する予定」
- ✅ 「開発メンバーが3人以上」
- ✅ 「データベースや外部APIの変更が予想される」
- ✅ 「ビジネスルールが10個以上ある」
- ✅ 「ユニットテストの保守性を重視したい」
- ✅ 「異なるチームが同じコードベースを触る」
逆に、以下の場合はシンプルなアーキテクチャで十分:
- MVPやプロトタイプの段階
- シンプルなCRUDアプリケーション
- 個人開発の小規模プロジェクト
オニオンアーキテクチャは 「ビジネスロジックを技術から守る」 ための強力な防波堤です。依存関係逆転のシンプルな原理を理解し、段階的に導入することで、「変更に強いシステム」が構築できます。
最初は時間がかかるかもしれませんが、長期的には「ビジネスロジックの修正がスムーズ」「テストが書きやすい」「新人がコードを理解しやすい」といった大きなメリットを実感できるはずです。
最後に
エンジニア募集
Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!