- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 1. なぜアーキテクチャが<ruby>重要<rp>(</rp><rt>じゅうよう</rt><rp>)</rp></ruby>なのか
- 1.1 <ruby>技術的<rp>(</rp><rt>ぎじゅつてき</rt><rp>)</rp></ruby><ruby>負債<rp>(</rp><rt>ふさい</rt><rp>)</rp></ruby>の<ruby>実態<rp>(</rp><rt>じったい</rt><rp>)</rp></ruby>
- 1.2 <ruby>良<rp>(</rp><rt>よ</rt><rp>)</rp></ruby>いアーキテクチャの4つの<ruby>特性<rp>(</rp><rt>とくせい</rt><rp>)</rp></ruby>
- 1.3 シニア<ruby>面接<rp>(</rp><rt>めんせつ</rt><rp>)</rp></ruby>でのアーキテクチャ
- 2. SOLID<ruby>原則<rp>(</rp><rt>げんそく</rt><rp>)</rp></ruby><ruby>深掘<rp>(</rp><rt>ふかぼ</rt><rp>)</rp></ruby>り
- 2.1 S — <ruby>単一<rp>(</rp><rt>たんいつ</rt><rp>)</rp></ruby><ruby>責任<rp>(</rp><rt>せきにん</rt><rp>)</rp></ruby>の<ruby>原則<rp>(</rp><rt>げんそく</rt><rp>)</rp></ruby>(Single Responsibility Principle)
- 2.2 O — <ruby>開放<rp>(</rp><rt>かいほう</rt><rp>)</rp></ruby><ruby>閉鎖<rp>(</rp><rt>へいさ</rt><rp>)</rp></ruby>の<ruby>原則<rp>(</rp><rt>げんそく</rt><rp>)</rp></ruby>(Open/Closed Principle)
- 2.3 L — リスコフの<ruby>置換<rp>(</rp><rt>ちかん</rt><rp>)</rp></ruby><ruby>原則<rp>(</rp><rt>げんそく</rt><rp>)</rp></ruby>(Liskov Substitution Principle)
- 2.4 I — インターフェース<ruby>分離<rp>(</rp><rt>ぶんり</rt><rp>)</rp></ruby>の<ruby>原則<rp>(</rp><rt>げんそく</rt><rp>)</rp></ruby>(Interface Segregation Principle)
- 2.5 D — <ruby>依存性<rp>(</rp><rt>いぞんせい</rt><rp>)</rp></ruby><ruby>逆転<rp>(</rp><rt>ぎゃくてん</rt><rp>)</rp></ruby>の<ruby>原則<rp>(</rp><rt>げんそく</rt><rp>)</rp></ruby>(Dependency Inversion Principle)
- 3. レイヤードアーキテクチャ
- 4. クリーンアーキテクチャ(Uncle Bob)
- 4.1 <ruby>核心<rp>(</rp><rt>かくしん</rt><rp>)</rp></ruby><ruby>概念<rp>(</rp><rt>がいねん</rt><rp>)</rp></ruby>:<ruby>同心円<rp>(</rp><rt>どうしんえん</rt><rp>)</rp></ruby><ruby>構造<rp>(</rp><rt>こうぞう</rt><rp>)</rp></ruby>
- 4.2 <ruby>各層<rp>(</rp><rt>かくそう</rt><rp>)</rp></ruby>の<ruby>役割<rp>(</rp><rt>やくわり</rt><rp>)</rp></ruby>
- 4.3 Spring Boot プロジェクト<ruby>構造<rp>(</rp><rt>こうぞう</rt><rp>)</rp></ruby>
- 4.4 <ruby>依存性<rp>(</rp><rt>いぞんせい</rt><rp>)</rp></ruby>ルールの<ruby>威力<rp>(</rp><rt>いりょく</rt><rp>)</rp></ruby>
- 5. ヘキサゴナルアーキテクチャ(ポート&アダプター)
- 5.1 Alistair Cockburnの<ruby>元々<rp>(</rp><rt>もともと</rt><rp>)</rp></ruby>の<ruby>概念<rp>(</rp><rt>がいねん</rt><rp>)</rp></ruby>
- 5.2 ポートとアダプターの<ruby>概念<rp>(</rp><rt>がいねん</rt><rp>)</rp></ruby>
- 5.3 <ruby>全体<rp>(</rp><rt>ぜんたい</rt><rp>)</rp></ruby><ruby>実装<rp>(</rp><rt>じっそう</rt><rp>)</rp></ruby><ruby>例<rp>(</rp><rt>れい</rt><rp>)</rp></ruby>
- 5.4 クリーンアーキテクチャとの<ruby>比較<rp>(</rp><rt>ひかく</rt><rp>)</rp></ruby>
- 6. DDD(Domain-Driven Design)
- 6.1 DDDとは
- 6.2 Strategic DDD — <ruby>大<rp>(</rp><rt>おお</rt><rp>)</rp></ruby>きな<ruby>絵<rp>(</rp><rt>え</rt><rp>)</rp></ruby>の<ruby>設計<rp>(</rp><rt>せっけい</rt><rp>)</rp></ruby>
- 6.3 Tactical DDD — コードレベル<ruby>設計<rp>(</rp><rt>せっけい</rt><rp>)</rp></ruby>
- 6.4 Event Storming
- 6.5 DDD + Hexagonal = <ruby>強力<rp>(</rp><rt>きょうりょく</rt><rp>)</rp></ruby>な<ruby>組<rp>(</rp><rt>く</rt><rp>)</rp></ruby>み<ruby>合<rp>(</rp><rt>あ</rt><rp>)</rp></ruby>わせ
- 6.6 DDDを<ruby>使<rp>(</rp><rt>つか</rt><rp>)</rp></ruby>うべきでないとき
- 7. アーキテクチャ<ruby>比較<rp>(</rp><rt>ひかく</rt><rp>)</rp></ruby><ruby>総整理<rp>(</rp><rt>そうせいり</rt><rp>)</rp></ruby>
- 8. <ruby>実践<rp>(</rp><rt>じっせん</rt><rp>)</rp></ruby>プロジェクト:Eコマース<ruby>注文<rp>(</rp><rt>ちゅうもん</rt><rp>)</rp></ruby>システム
- 9. テスト<ruby>戦略<rp>(</rp><rt>せんりゃく</rt><rp>)</rp></ruby>
- 10. アンチパターンと<ruby>注意点<rp>(</rp><rt>ちゅういてん</rt><rp>)</rp></ruby>
- 10.1 Over-Engineering(<ruby>過剰<rp>(</rp><rt>かじょう</rt><rp>)</rp></ruby>エンジニアリング)
- 10.2 Anemic Domain Model(<ruby>貧血<rp>(</rp><rt>ひんけつ</rt><rp>)</rp></ruby>ドメインモデル)
- 10.3 God Class(<ruby>神<rp>(</rp><rt>かみ</rt><rp>)</rp></ruby>クラス)
- 10.4 Leaky Abstraction(<ruby>抽象化<rp>(</rp><rt>ちゅうしょうか</rt><rp>)</rp></ruby>の<ruby>漏<rp>(</rp><rt>も</rt><rp>)</rp></ruby>れ)
- 10.5 Resume-Driven Architecture(<ruby>履歴書<rp>(</rp><rt>りれきしょ</rt><rp>)</rp></ruby><ruby>駆動<rp>(</rp><rt>くどう</rt><rp>)</rp></ruby>アーキテクチャ)
- クイズ
- <ruby>参考<rp>(</rp><rt>さんこう</rt><rp>)</rp></ruby><ruby>資料<rp>(</rp><rt>しりょう</rt><rp>)</rp></ruby>
はじめに
ソフトウェア開発で最もコストがかかるのは何でしょうか?初期開発ではありません。保守です。研究によると、ソフトウェアライフサイクルコストの60〜80%が保守に費やされます。そして保守コストを決定する最大の要因がアーキテクチャです。
Robert C. Martin(Uncle Bob)はこう述べました。「良いアーキテクチャはシステムを理解しやすく、開発しやすく、保守しやすく、デプロイしやすくする。」これは単なる格言ではなく、数十年にわたるソフトウェアプロジェクトの成功と失敗を貫く核心的な原則です。
シニアエンジニアの面接で「クリーンアーキテクチャとヘキサゴナルアーキテクチャの違いを説明してください」「DDDのBounded Contextとは何ですか?」といった質問が必ず出る理由がここにあります。アーキテクチャを理解することは、コードを書く技術を超えて、**システムを設計する能力**を持つということです。
この記事では、SOLID原則からレイヤード、クリーン、ヘキサゴナル、DDDまで — 現代ソフトウェア設計の核心パターンを実践コードと共に深層分析します。
1. なぜアーキテクチャが重要なのか
1.1 技術的負債の実態
技術的
負債
(Technical Debt)は
時間
とともに
複利
で
膨
らみます。
初期
にアーキテクチャなしで
素早
く
開発
したプロジェクトは
以下
のようなコスト
曲線
を
描
きます。
コスト
^
| / アーキテクチャなし
| /
| /
| /
| / ___--- 良いアーキテクチャ
| / ---
| / --
| /--
+-------------------> 時間
Stripeの2018年の調査によると、開発者は業務時間の約33%を技術的負債の処理に費やしています。
1.2 良いアーキテクチャの4つの特性
Uncle Bobが定義した良いアーキテクチャの特性は以下の通りです。
- 理解しやすい — 新しいチームメンバーが素早くシステムを把握
- 開発しやすい — 機能追加の影響範囲が限定的
- 保守しやすい — バグ修正とリファクタリングが安全
- デプロイしやすい — 独立したデプロイとロールバックが可能
1.3 シニア面接でのアーキテクチャ
シニアエンジニアの面接でアーキテクチャの質問は必須です。よく出るテーマは以下です。
- SOLID原則を実際のプロジェクトでどう適用したか
- レイヤード vs クリーン vs ヘキサゴナルの違いと選択基準
- DDDの適用経験とBounded Context設計
-
技術的
負債
をどう管理
・解消
したか
**実践経験に基づいた深い回答**ができなければなりません。
2. SOLID原則深掘り
SOLIDはRobert C. Martinが整理したオブジェクト指向設計の5つの核心原則です。すべてのアーキテクチャパターンの基礎になります。
2.1 S — 単一責任の原則(Single Responsibility Principle)
クラスは変更の理由が1つであるべきだ。
核心
は「1つのことだけを
行
う」ではなく、**「
変更
の
理由
が1つ」**ということです。
BAD: 複数の変更理由を持つクラス
// BAD: UserServiceがビジネスロジック、永続化、通知をすべて担当
class UserService {
createUser(name: string, email: string) {
// バリデーション
if (!email.includes('@')) throw new Error('Invalid email')
// DB保存(永続化の関心事)
const query = `INSERT INTO users (name, email) VALUES ('${name}', '${email}')`
database.execute(query)
// メール送信(通知の関心事)
const mailClient = new SmtpClient()
mailClient.send(email, 'Welcome!', 'Thanks for joining')
// ロギング(インフラの関心事)
console.log(`User created: ${name}`)
}
}
GOOD: 責任を分離した構造
// GOOD: 各クラスが1つの変更理由のみを持つ
class UserValidator {
validate(name: string, email: string): void {
if (!email.includes('@')) throw new Error('Invalid email')
if (name.length < 2) throw new Error('Name too short')
}
}
class UserRepository {
save(user: User): Promise<User> {
return this.db.users.create({ data: user })
}
}
class WelcomeEmailSender {
send(email: string): Promise<void> {
return this.mailService.send({
to: email,
subject: 'Welcome!',
body: 'Thanks for joining',
})
}
}
class CreateUserUseCase {
constructor(
private validator: UserValidator,
private repository: UserRepository,
private emailSender: WelcomeEmailSender
) {}
async execute(name: string, email: string): Promise<User> {
this.validator.validate(name, email)
const user = await this.repository.save({ name, email })
await this.emailSender.send(email)
return user
}
}
2.2 O — 開放閉鎖の原則(Open/Closed Principle)
拡張
に
対
しては
開
いており、
修正
に
対
しては
閉
じていなければならない。
BAD: 新しい決済手段追加のたびに既存コード修正が必要
// BAD: 新しい決済手段ごとにif文追加が必要
class PaymentProcessor {
process(payment: Payment) {
if (payment.type === 'credit_card') {
this.processCreditCard(payment)
} else if (payment.type === 'paypal') {
this.processPayPal(payment)
} else if (payment.type === 'bitcoin') {
// 新しい決済手段追加のたびに既存コード修正!
this.processBitcoin(payment)
}
}
}
GOOD: ストラテジーパターンで拡張に開く
// GOOD: 新しい決済手段追加時に既存コード修正不要
interface PaymentStrategy {
process(payment: Payment): Promise<PaymentResult>
}
class CreditCardStrategy implements PaymentStrategy {
async process(payment: Payment): Promise<PaymentResult> {
return { success: true, transactionId: 'cc-123' }
}
}
class PayPalStrategy implements PaymentStrategy {
async process(payment: Payment): Promise<PaymentResult> {
return { success: true, transactionId: 'pp-456' }
}
}
// 新しい決済手段 = 新しいクラスを追加するだけ
class PayPayStrategy implements PaymentStrategy {
async process(payment: Payment): Promise<PaymentResult> {
return { success: true, transactionId: 'pp-789' }
}
}
class PaymentProcessor {
private strategies: Map<string, PaymentStrategy>
constructor(strategies: Map<string, PaymentStrategy>) {
this.strategies = strategies
}
async process(payment: Payment): Promise<PaymentResult> {
const strategy = this.strategies.get(payment.type)
if (!strategy) throw new Error(`Unknown payment type: ${payment.type}`)
return strategy.process(payment)
}
}
2.3 L — リスコフの置換原則(Liskov Substitution Principle)
サブタイプはベースタイプの代わりに使えなければならない。
BAD: 正方形-長方形問題
// BAD: SquareがRectangleの不変条件を違反
class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
// LSP違反:setWidthがheightも変更
@Override
public void setWidth(int w) {
this.width = w;
this.height = w; // 親と異なる動作!
}
@Override
public void setHeight(int h) {
this.width = h;
this.height = h;
}
}
// クライアントコードが壊れる
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
assert r.getArea() == 50; // Squareだと失敗!(100)
}
GOOD: 共通インターフェースで解決
// GOOD: 共通インターフェースで設計
interface Shape {
int getArea();
}
class Rectangle implements Shape {
private final int width;
private final int height;
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() { return width * height; }
}
class Square implements Shape {
private final int side;
Square(int side) { this.side = side; }
public int getArea() { return side * side; }
}
2.4 I — インターフェース分離の原則(Interface Segregation Principle)
クライアントは使わないメソッドに依存すべきではない。
BAD: 太いインターフェース
// BAD: すべてのデバイスがすべての機能を実装しなければならない
interface SmartDevice {
print(): void
scan(): void
fax(): void
copyDocument(): void
}
// プリンターはscan、faxを実装できない
class SimplePrinter implements SmartDevice {
print() {
/* OK */
}
scan() {
throw new Error('Not supported')
} // 強制実装
fax() {
throw new Error('Not supported')
} // 強制実装
copyDocument() {
throw new Error('Not supported')
}
}
GOOD: 分離されたインターフェース
// GOOD: 役割別に分離されたインターフェース
interface Printer {
print(): void
}
interface Scanner {
scan(): void
}
interface FaxMachine {
fax(): void
}
// 必要なインターフェースのみ実装
class SimplePrinter implements Printer {
print() {
/* 印刷処理 */
}
}
class AllInOnePrinter implements Printer, Scanner, FaxMachine {
print() {
/* 印刷 */
}
scan() {
/* スキャン */
}
fax() {
/* ファックス */
}
}
2.5 D — 依存性逆転の原則(Dependency Inversion Principle)
上位
モジュールは
下位
モジュールに
依存
してはならない。
両方
とも
抽象
に
依存
すべきだ。
すべてのアーキテクチャパターンの核心です。
BAD: 上位が下位に直接依存
// BAD: ビジネスロジックが具体的な実装に依存
class OrderService {
private mysqlDb = new MySQLDatabase() // 具象クラスに依存
private smtpMail = new SmtpEmailClient() // 具象クラスに依存
createOrder(order: Order) {
this.mysqlDb.insert('orders', order)
this.smtpMail.send(order.userEmail, 'Order confirmed')
}
}
// MySQLをPostgreSQLに変えるにはOrderServiceの修正が必要!
GOOD: 抽象に依存
// GOOD: インターフェース(抽象)に依存
interface OrderRepository {
save(order: Order): Promise<Order>
}
interface NotificationService {
notify(userId: string, message: string): Promise<void>
}
class OrderService {
constructor(
private repository: OrderRepository, // 抽象に依存
private notification: NotificationService // 抽象に依存
) {}
async createOrder(order: Order): Promise<Order> {
const saved = await this.repository.save(order)
await this.notification.notify(order.userId, 'Order confirmed')
return saved
}
}
// インフラ層で実装
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<Order> {
return this.prisma.order.create({ data: order })
}
}
3. レイヤードアーキテクチャ
3.1 基本構造
レイヤードアーキテクチャは最も伝統的で広く使われているパターンです。層間の明確な責任分離が核心です。
+-----------------------------+
| Presentation Layer | <- Controller, View
+-----------------------------+
| Application Layer | <- Service, DTO
+-----------------------------+
| Domain Layer | <- Entity, Business Logic
+-----------------------------+
| Infrastructure Layer | <- Repository, External API
+-----------------------------+
依存性の方向: 上 -> 下 (Presentation -> Infrastructure)
3.2 実装例
// Presentation Layer
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody CreateOrderRequest request) {
OrderResponse response = orderService.createOrder(request);
return ResponseEntity.status(201).body(response);
}
}
// Application Layer
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public OrderResponse createOrder(CreateOrderRequest request) {
Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new NotFoundException("Product not found"));
Order order = Order.create(
request.getUserId(),
product,
request.getQuantity()
);
Order saved = orderRepository.save(order);
return OrderResponse.from(saved);
}
}
// Domain Layer
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
private String userId;
private BigDecimal totalAmount;
private OrderStatus status;
public static Order create(String userId, Product product, int quantity) {
Order order = new Order();
order.userId = userId;
order.totalAmount = product.getPrice().multiply(BigDecimal.valueOf(quantity));
order.status = OrderStatus.CREATED;
return order;
}
public void cancel() {
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("Only CREATED orders can be cancelled");
}
this.status = OrderStatus.CANCELLED;
}
}
3.3 長所と短所
長所:
-
理解
しやすい — ほとんどの開発者
に馴染
みがある -
開発
が速
い — シンプルなCRUDアプリに適
している -
関心事
の分離
—基本的
なレイヤー分離
を提供
短所:
- ドメインロジックがサービス層に散らばりやすい(Anemic Domain Model)
- DB変更の影響範囲が大きい
- テスト時にインフラ依存性のモックが必要
4. クリーンアーキテクチャ(Uncle Bob)
4.1 核心概念:同心円構造
2012年にRobert C. Martinが発表したクリーンアーキテクチャは、依存性ルール(Dependency Rule)を核心とします。すべての依存性は内側を向く必要があります。
+--------------------------------------------------+
| Frameworks & Drivers (Web, DB, External) |
| +------------------------------------------+ |
| | Interface Adapters (Controllers, GW) | |
| | +----------------------------------+ | |
| | | Application (Use Cases) | | |
| | | +------------------------+ | | |
| | | | Entities | | | |
| | | | (Domain Models) | | | |
| | | +------------------------+ | | |
| | +----------------------------------+ | |
| +------------------------------------------+ |
+--------------------------------------------------+
依存性の方向: 外側 -> 内側 (Frameworks -> Entities)
4.2 各層の役割
Entities(エンティティ): ビジネスルールをカプセル化。最も変化しにくい核心ロジック。
// Entities - いかなるフレームワークにも依存しない
class Order {
private items: OrderItem[] = []
private status: OrderStatus = 'CREATED'
addItem(product: Product, quantity: number): void {
if (quantity <= 0) throw new Error('Quantity must be positive')
if (this.status !== 'CREATED') throw new Error('Cannot modify confirmed order')
this.items.push(new OrderItem(product, quantity))
}
get totalAmount(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('KRW'))
}
confirm(): void {
if (this.items.length === 0) throw new Error('Cannot confirm empty order')
this.status = 'CONFIRMED'
}
}
Use Cases(ユースケース): アプリケーション固有のビジネスルール。エンティティを組み合わせてシナリオを実装。
// Use Cases
interface OrderRepository {
save(order: Order): Promise<Order>
findById(id: string): Promise<Order | null>
}
interface PaymentGateway {
charge(amount: Money, paymentMethod: PaymentMethod): Promise<PaymentResult>
}
class CreateOrderUseCase {
constructor(
private orderRepo: OrderRepository,
private paymentGateway: PaymentGateway,
private eventPublisher: DomainEventPublisher
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const order = new Order(input.userId)
for (const item of input.items) {
order.addItem(item.product, item.quantity)
}
order.confirm()
const paymentResult = await this.paymentGateway.charge(order.totalAmount, input.paymentMethod)
if (!paymentResult.success) {
throw new PaymentFailedError(paymentResult.reason)
}
const saved = await this.orderRepo.save(order)
await this.eventPublisher.publish(new OrderCreatedEvent(saved.id))
return { orderId: saved.id, totalAmount: order.totalAmount.toString() }
}
}
4.3 Spring Boot プロジェクト構造
src/main/java/com/example/shop/
domain/ # Entities
model/
Order.java
OrderItem.java
Money.java
event/
OrderCreatedEvent.java
application/ # Use Cases
port/
in/
CreateOrderUseCase.java
out/
OrderRepository.java
PaymentGateway.java
service/
OrderApplicationService.java
adapter/ # Interface Adapters + Frameworks
in/
web/
OrderController.java
CreateOrderRequest.java
out/
persistence/
JpaOrderRepository.java
OrderJpaEntity.java
payment/
StripePaymentGateway.java
config/
BeanConfiguration.java
4.4 依存性ルールの威力
クリーンアーキテクチャの核心は**依存性ルール**です。内側の円は外側の円について何も知りません。
// domain層 - 外部依存性 ZERO
// Order.tsのimport文を見ると:
// - express なし
// - prisma なし
// - typeorm なし
// 純粋なTypeScript/Javaコードのみ
class Order {
// 純粋なビジネスロジックのみ含む
// DBがMySQLでもMongoDBでも関係ない
// WebフレームワークがExpressでもFastifyでも関係ない
}
5. ヘキサゴナルアーキテクチャ(ポート&アダプター)
5.1 Alistair Cockburnの元々の概念
ヘキサゴナルアーキテクチャは2005年にAlistair Cockburnが提案したパターンで、正式な名前は「Ports and Adapters」です。六角形の形はポートの数が柔軟であることを表現しており、正確に6つという意味ではありません。
+----------+
HTTP --> | | --> PostgreSQL
| Port |
CLI --> | & | --> Redis
| Domain |
gRPC --> | Core | --> Kafka
| |
Test --> | | --> Mock DB
+----------+
[Driving Adapters] [Core] [Driven Adapters]
(入力側) (ビジネス) (出力側)
5.2 ポートとアダプターの概念
ポート(Port): ビジネスロジックが外部と通信するためのインターフェース。
- Driving Port(インバウンドポート): 外部がアプリケーションを呼び出すインターフェース
- Driven Port(アウトバウンドポート): アプリケーションが外部システムを呼び出すインターフェース
アダプター(Adapter): ポートの具体的な実装。
- Driving Adapter: REST Controller、CLI、gRPC Handler
- Driven Adapter: JPA Repository、HTTP Client、Message Publisher
5.3 全体実装例
// ===== PORTS(インターフェース定義) =====
// Driving Port(インバウンド)
interface CreateOrderPort {
createOrder(command: CreateOrderCommand): Promise<OrderId>
}
// Driven Port(アウトバウンド)
interface LoadOrderPort {
findById(id: string): Promise<Order | null>
}
interface SaveOrderPort {
save(order: Order): Promise<Order>
}
interface SendNotificationPort {
sendOrderConfirmation(order: Order): Promise<void>
}
// ===== DOMAIN CORE(核心ビジネスロジック) =====
class Order {
private readonly id: string
private items: OrderItem[] = []
private status: OrderStatus = 'PENDING'
constructor(
id: string,
private readonly customerId: string
) {
this.id = id
}
addItem(productId: string, price: Money, quantity: number): void {
this.ensureModifiable()
this.items.push(new OrderItem(productId, price, quantity))
}
confirm(): void {
if (this.items.length === 0) {
throw new DomainError('Cannot confirm empty order')
}
this.status = 'CONFIRMED'
}
cancel(): void {
if (this.status === 'SHIPPED') {
throw new DomainError('Cannot cancel shipped order')
}
this.status = 'CANCELLED'
}
private ensureModifiable(): void {
if (this.status !== 'PENDING') {
throw new DomainError('Order is not modifiable')
}
}
get totalAmount(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('KRW'))
}
}
// ===== APPLICATION SERVICE(ユースケース実装) =====
class OrderService implements CreateOrderPort {
constructor(
private readonly loadOrder: LoadOrderPort,
private readonly saveOrder: SaveOrderPort,
private readonly notification: SendNotificationPort
) {}
async createOrder(command: CreateOrderCommand): Promise<OrderId> {
const order = new Order(generateId(), command.customerId)
for (const item of command.items) {
order.addItem(item.productId, new Money(item.price, 'KRW'), item.quantity)
}
order.confirm()
const saved = await this.saveOrder.save(order)
await this.notification.sendOrderConfirmation(saved)
return saved.id
}
}
// ===== ADAPTERS(具体的な実装) =====
// Driving Adapter - REST Controller
class OrderRestController {
constructor(private createOrderPort: CreateOrderPort) {}
async handlePost(req: Request, res: Response): Promise<void> {
const command: CreateOrderCommand = {
customerId: req.body.customerId,
items: req.body.items,
}
const orderId = await this.createOrderPort.createOrder(command)
res.status(201).json({ orderId })
}
}
// Driven Adapter - Database
class PostgresOrderAdapter implements LoadOrderPort, SaveOrderPort {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<Order | null> {
const data = await this.prisma.order.findUnique({
where: { id },
include: { items: true },
})
return data ? this.toDomain(data) : null
}
async save(order: Order): Promise<Order> {
await this.prisma.order.upsert({
where: { id: order.id },
create: this.toPersistence(order),
update: this.toPersistence(order),
})
return order
}
private toDomain(data: PrismaOrder): Order {
/* マッピング */
}
private toPersistence(order: Order): PrismaOrderData {
/* マッピング */
}
}
5.4 クリーンアーキテクチャとの比較
| 観点 | クリーンアーキテクチャ | ヘキサゴナルアーキテクチャ |
|---|---|---|
| 提案者 | Robert C. Martin (2012) | Alistair Cockburn (2005) |
| 視覚化 | 同心円 | 六角形 |
| 核心アイデア | 依存性が内側を向く | ポートとアダプターで分離 |
| 層の数 | 4つ | 3つ |
| 強調点 | 依存性ルール | 入出力の対称性 |
| 本質 | 同じ原則の異なる表現 | 同じ原則の異なる表現 |
両方
とも**
依存性
逆転
**という
同
じ
原則
に
基
づいています。
6. DDD(Domain-Driven Design)
6.1 DDDとは
DDD(Domain-Driven Design)はEric Evansが2003年の著書で紹介した方法論で、複雑なビジネスドメインをソフトウェアモデルの中心に置くアプローチです。
6.2 Strategic DDD — 大きな絵の設計
Ubiquitous Language(ユビキタス言語)
開発者
とビジネス
専門家
が
同
じ
用語
を
使用
すること。コードのクラス
名
、メソッド
名
がビジネス
用語
と
一致
する
必要
があります。
ビジネス: 「顧客が注文を作成し、決済が承認されると注文が確定される」
コード: customer.createOrder() -> payment.approve() -> order.confirm()
ビジネス: 「配送が開始されると注文をキャンセルできない」
コード: order.cancel() -> if (status === 'SHIPPED') throw Error
Bounded Context(バウンデッドコンテキスト)
1つのシステム内でも同じ用語が異なる意味を持つことがあります。Bounded Contextは特定のモデルが有効な境界を明示します。
+------------------+ +------------------+ +------------------+
| Order Context | | Payment Context | | Shipping Context |
| | | | | |
| Order | | Payment | | Shipment |
| OrderItem | | Transaction | | Package |
| Customer (id, | | Customer (id, | | Customer (id, |
| name, email) | | paymentMethod) | | address) |
| | | | | |
| 「Customer」= | | 「Customer」= | | 「Customer」= |
| 注文者情報 | | 決済者情報 | | 受取人情報 |
+--------+---------+ +--------+---------+ +--------+---------+
6.3 Tactical DDD — コードレベル設計
Entity(エンティティ)
固有
の
識別子
を
持
ち、ライフサイクルを
通
じて
識別子
が
維持
されるオブジェクト。
public class Order {
private final OrderId id; // 固有識別子
private OrderStatus status;
private List<OrderItem> items;
// ビジネスロジックがエンティティ内に存在(Rich Domain Model)
public void addItem(Product product, int quantity) {
validateModifiable();
OrderItem item = new OrderItem(product, quantity);
this.items.add(item);
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new OrderEmptyException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(this.id));
}
}
Value Object(値オブジェクト)
識別子
がなく、
属性
値
で
等価性
を
判断
。
不変
(Immutable)。
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
Objects.requireNonNull(currency);
}
public Money add(Money other) {
validateSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
public static Money zero(Currency currency) {
return new Money(BigDecimal.ZERO, currency);
}
}
// 使用例
Money price = new Money(BigDecimal.valueOf(15000), Currency.JPY);
Money total = price.multiply(3); // 45000 JPY
Aggregate(アグリゲート)
関連
するエンティティと
値
オブジェクトのクラスター。Aggregate Rootを
通
してのみアクセス
可能
。
// OrderがAggregate Root
// OrderItemはOrderを通してのみアクセス/修正可能
public class Order { // Aggregate Root
private final OrderId id;
private final CustomerId customerId;
private List<OrderItem> items = new ArrayList<>(); // 内部エンティティ
private ShippingAddress address; // 値オブジェクト
private Money totalAmount; // 値オブジェクト
// 外部からOrderItemを直接生成/修正不可
// 必ずOrderを通して操作
public void addItem(ProductId productId, Money price, int qty) {
OrderItem item = new OrderItem(productId, price, qty);
this.items.add(item);
recalculateTotal();
}
}
Aggregate設計ルール:
- Aggregate外部からはRoot Entityのみ参照
- Aggregate間の参照はIDのみ(直接オブジェクト参照禁止)
- 1つのトランザクションで1つのAggregateのみ修正
- Aggregateは小さく保つ(1 Root + 少数の内部オブジェクト)
Domain Event(ドメインイベント)
ドメインで発生した重要な事象を表すオブジェクト。
public record OrderConfirmedEvent(
OrderId orderId,
CustomerId customerId,
Money totalAmount,
Instant occurredAt
) implements DomainEvent {}
// Aggregate Rootでイベント登録
public class Order extends AbstractAggregateRoot<Order> {
public void confirm() {
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount));
}
}
// イベントハンドラー
@Component
public class OrderConfirmedEventHandler {
@EventListener
public void handleOrderConfirmed(OrderConfirmedEvent event) {
// 決済処理トリガー
// 在庫差し引きトリガー
// 確認メール送信
}
}
6.4 Event Storming
Event Stormingは、DDDでドメインを探索しBounded Contextを発見するためのワークショップ技法です。
1. Domain Events(オレンジの付箋) - 過去形で記述
「注文が作成された」 「決済が承認された」 「商品が配送された」
2. Commands(青の付箋) - イベントを発生させる行為
「注文作成」 「決済承認」 「配送開始」
3. Aggregates(黄色の付箋) - コマンドを処理する主体
「Order」 「Payment」 「Shipment」
4. Bounded Contexts発見 - 関連するものをグルーピング
Order Context | Payment Context | Shipping Context
6.5 DDD + Hexagonal = 強力な組み合わせ
DDDの戦術的パターンをヘキサゴナルアーキテクチャに配置すると最も強力な組み合わせになります。
src/
order/ # Bounded Context
domain/ # ヘキサゴナルのCore
model/
Order.java # Aggregate Root
OrderItem.java # Entity
Money.java # Value Object
event/
OrderConfirmedEvent.java # Domain Event
application/ # ポート定義 + Use Case
port/
in/
CreateOrderUseCase.java # Driving Port
out/
LoadOrderPort.java # Driven Port
SaveOrderPort.java
service/
OrderApplicationService.java # Use Case実装
infrastructure/ # アダプター
adapter/
in/
web/
OrderController.java # Driving Adapter
out/
persistence/
JpaOrderRepository.java # Driven Adapter
payment/
StripePaymentAdapter.java
6.6 DDDを使うべきでないとき
DDDは強力ですが、すべてのプロジェクトに適しているわけではありません。
使うべきでない場合:
- シンプルなCRUDアプリケーション
- ビジネスロジックがほとんどないデータパイプライン
- プロトタイプやMVP
- チームがDDDへの理解が不足している場合
- ドメイン専門家との協業が不可能な場合
7. アーキテクチャ比較総整理
7.1 比較表
| 項目 | レイヤード | クリーン | ヘキサゴナル | DDD |
|---|---|---|---|---|
| 複雑度 | 低い | 中 | 中-高 | 高い |
| 学習コスト | 低い | 中 | 中 | 高い |
| 適したプロジェクト | シンプルなCRUD | 中程度の複雑度 | 複雑なドメイン | 複雑なビジネスロジック |
| 核心利点 | 理解が容易 | フレームワーク独立 | アダプター交換可能 | ビジネス整合 |
| テスト容易性 | 普通 | 高い | 高い | 非常に高い |
| DB交換容易性 | 困難 | 容易 | 非常に容易 | 非常に容易 |
7.2 選択フローチャート
プロジェクト開始
|
+-- ビジネスロジックが単純? --YES--> レイヤードアーキテクチャ
|
NO
|
+-- 外部システム統合が多い? --YES--> ヘキサゴナルアーキテクチャ
|
NO
|
+-- ドメイン専門家との協業可能? --YES--+
| |
NO v
| ビジネスロジックが非常に複雑?
| | |
| YES NO
| | |
| v v
| DDD + Hexagonal クリーンアーキテクチャ
|
+--> クリーンアーキテクチャ(安全なデフォルト選択)
7.3 実務ヒント
- 段階的導入: 最初からDDDを全体に適用せず、最も複雑なモジュールから開始
- 混合使用: 1つのシステム内でモジュールごとに異なるアーキテクチャを適用可能
- チーム能力考慮: チームの経験と理解度に合ったアーキテクチャを選択
- YAGNI原則: 今必要ない抽象化を先に作らない
8. 実践プロジェクト:Eコマース注文システム
8.1 ドメインモデル設計
Clean/Hexagonal + DDDを適用したEコマース注文システムの全体実装です。
Money(Value Object)
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount, "Amount must not be null");
Objects.requireNonNull(currency, "Currency must not be null");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
public static Money of(long amount, String currency) {
return new Money(BigDecimal.valueOf(amount), currency);
}
public static Money zero(String currency) {
return new Money(BigDecimal.ZERO, currency);
}
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
private void assertSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException(
"Currency mismatch: " + this.currency + " vs " + other.currency
);
}
}
}
Order(Aggregate Root)
public class Order {
private final String id;
private final String customerId;
private final List<OrderItem> items = new ArrayList<>();
private OrderStatus status;
private Money totalAmount;
private final List<DomainEvent> domainEvents = new ArrayList<>();
public Order(String id, String customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.PENDING;
this.totalAmount = Money.zero("JPY");
}
public void addItem(String productId, String productName, Money price, int quantity) {
ensureModifiable();
this.items.add(new OrderItem(productId, productName, price, quantity));
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new OrderDomainException("Cannot confirm empty order");
}
if (status != OrderStatus.PENDING) {
throw new OrderDomainException("Only pending orders can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
domainEvents.add(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount));
}
public void cancel() {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
throw new OrderDomainException("Cannot cancel order in status: " + status);
}
this.status = OrderStatus.CANCELLED;
domainEvents.add(new OrderCancelledEvent(this.id, this.customerId));
}
public void markAsShipped(String trackingNumber) {
if (status != OrderStatus.CONFIRMED) {
throw new OrderDomainException("Only confirmed orders can be shipped");
}
this.status = OrderStatus.SHIPPED;
domainEvents.add(new OrderShippedEvent(this.id, trackingNumber));
}
private void ensureModifiable() {
if (status != OrderStatus.PENDING) {
throw new OrderDomainException("Order is not modifiable");
}
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.zero("JPY"), Money::add);
}
}
8.2 Application Layer(Use Cases)
// Driving Port
public interface CreateOrderUseCase {
CreateOrderResult execute(CreateOrderCommand command);
}
// Driven Ports
public interface OrderRepository {
Optional<Order> findById(String id);
Order save(Order order);
}
public interface EventPublisher {
void publish(List<DomainEvent> events);
}
// Use Case実装
@Service
@Transactional
public class CreateOrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
private final IdGenerator idGenerator;
@Override
public CreateOrderResult execute(CreateOrderCommand command) {
Order order = new Order(idGenerator.generate(), command.customerId());
for (OrderItemCommand item : command.items()) {
order.addItem(
item.productId(),
item.productName(),
Money.of(item.price(), "JPY"),
item.quantity()
);
}
order.confirm();
Order saved = orderRepository.save(order);
eventPublisher.publish(saved.getDomainEvents());
saved.clearDomainEvents();
return new CreateOrderResult(saved.getId(), saved.getTotalAmount().toString());
}
}
8.3 Infrastructure Layer(Adapters)
// Driven Adapter - JPA Repository
@Repository
public class JpaOrderRepositoryAdapter implements OrderRepository {
private final SpringDataOrderRepository jpaRepo;
private final OrderPersistenceMapper mapper;
@Override
public Optional<Order> findById(String id) {
return jpaRepo.findById(id).map(mapper::toDomain);
}
@Override
public Order save(Order order) {
OrderJpaEntity entity = mapper.toJpaEntity(order);
OrderJpaEntity saved = jpaRepo.save(entity);
return mapper.toDomain(saved);
}
}
// Driven Adapter - Kafka Event Publisher
@Component
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
@Override
public void publish(List<DomainEvent> events) {
for (DomainEvent event : events) {
String topic = resolveTopic(event);
String payload = serialize(event);
kafkaTemplate.send(topic, event.getAggregateId(), payload);
}
}
private String resolveTopic(DomainEvent event) {
if (event instanceof OrderConfirmedEvent) return "order.confirmed";
if (event instanceof OrderCancelledEvent) return "order.cancelled";
if (event instanceof OrderShippedEvent) return "order.shipped";
return "order.unknown";
}
}
// Driving Adapter - REST Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
private final CancelOrderUseCase cancelOrderUseCase;
@PostMapping
public ResponseEntity<CreateOrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
CreateOrderCommand command = request.toCommand();
CreateOrderResult result = createOrderUseCase.execute(command);
return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
}
@DeleteMapping("/{orderId}")
public ResponseEntity<Void> cancelOrder(@PathVariable String orderId) {
cancelOrderUseCase.execute(orderId);
return ResponseEntity.noContent().build();
}
}
9. テスト戦略
9.1 ドメインロジックの単体テスト
ドメインロジックは**外部依存性なしで純粋に**テストできます。これがクリーン/ヘキサゴナルアーキテクチャの最大の利点です。
class OrderTest {
@Test
void shouldCalculateTotalAmount() {
Order order = new Order("order-1", "customer-1");
order.addItem("prod-1", "Keyboard", Money.of(50000, "JPY"), 2);
order.addItem("prod-2", "Mouse", Money.of(30000, "JPY"), 1);
assertEquals(Money.of(130000, "JPY"), order.getTotalAmount());
}
@Test
void shouldNotConfirmEmptyOrder() {
Order order = new Order("order-1", "customer-1");
assertThrows(OrderDomainException.class, () -> order.confirm());
}
@Test
void shouldNotCancelShippedOrder() {
Order order = createConfirmedOrder();
order.markAsShipped("TRACK-123");
assertThrows(OrderDomainException.class, () -> order.cancel());
}
@Test
void shouldRegisterDomainEventOnConfirm() {
Order order = new Order("order-1", "customer-1");
order.addItem("prod-1", "Keyboard", Money.of(50000, "JPY"), 1);
order.confirm();
List<DomainEvent> events = order.getDomainEvents();
assertEquals(1, events.size());
assertInstanceOf(OrderConfirmedEvent.class, events.get(0));
}
}
class MoneyTest {
@Test
void shouldAddSameCurrency() {
Money a = Money.of(1000, "JPY");
Money b = Money.of(2000, "JPY");
assertEquals(Money.of(3000, "JPY"), a.add(b));
}
@Test
void shouldRejectDifferentCurrency() {
Money jpy = Money.of(1000, "JPY");
Money usd = Money.of(1, "USD");
assertThrows(IllegalArgumentException.class, () -> jpy.add(usd));
}
}
9.2 Use Case統合テスト
class CreateOrderServiceTest {
private CreateOrderService sut;
private OrderRepository fakeRepository;
private EventPublisher fakePublisher;
@BeforeEach
void setUp() {
fakeRepository = new InMemoryOrderRepository();
fakePublisher = new FakeEventPublisher();
sut = new CreateOrderService(fakeRepository, fakePublisher, new UuidIdGenerator());
}
@Test
void shouldCreateAndConfirmOrder() {
CreateOrderCommand command = new CreateOrderCommand(
"customer-1",
List.of(new OrderItemCommand("prod-1", "Keyboard", 50000, 2))
);
CreateOrderResult result = sut.execute(command);
assertNotNull(result.orderId());
Order saved = fakeRepository.findById(result.orderId()).orElseThrow();
assertEquals(OrderStatus.CONFIRMED, saved.getStatus());
}
}
// テスト用In-Memory Repository
class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> store = new HashMap<>();
@Override
public Optional<Order> findById(String id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Order save(Order order) {
store.put(order.getId(), order);
return order;
}
}
9.3 ArchUnitによるアーキテクチャテスト
ArchUnitを使うと、アーキテクチャルールを自動的に検証できます。
class ArchitectureTest {
private final JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.shop");
@Test
void domainShouldNotDependOnInfrastructure() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..")
.check(classes);
}
@Test
void domainShouldNotDependOnApplication() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..application..")
.check(classes);
}
@Test
void domainShouldNotUseSpringAnnotations() {
noClasses()
.that().resideInAPackage("..domain..")
.should().beAnnotatedWith("org.springframework.stereotype.Service")
.orShould().beAnnotatedWith("org.springframework.stereotype.Component")
.check(classes);
}
}
9.4 テストピラミッド
/ E2E Tests \ <- 少なく、遅い、コスト高
/ (Cypress, etc) \
/------------------\
/ Integration Tests \ <- 中間
/ (Adapter Tests) \
/------------------------\
/ Unit Tests \ <- 多く、速い、コスト低
/ (Domain + Use Case) \
/-----------------------------\
クリーン/ヘキサゴナルアーキテクチャでは、ドメインロジックの単体テストが全体テストの大部分を占めます。
10. アンチパターンと注意点
10.1 Over-Engineering(過剰エンジニアリング)
最
もよくある
間違
いです。シンプルなCRUDアプリにDDD + Hexagonalを
適用
すると
生産性
がかえって
低下
します。
// BAD: ユーザー名を変えるのに7ファイルが必要?
// Command -> CommandHandler -> UseCase -> DomainService
// -> Repository -> Adapter -> Mapper
// 単純なアップデートには過剰!
// GOOD: 複雑度に合ったアーキテクチャを選択
// 単純なCRUD = レイヤード + サービスパターンで十分
10.2 Anemic Domain Model(貧血ドメインモデル)
エンティティにgetter/setterしかなく、ビジネスロジックがサービス層に散らばったパターン。Martin Fowlerが名付けたアンチパターンです。
// BAD: Anemic Domain Model
class Order {
private String status;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
class OrderService {
void cancelOrder(Order order) {
// ビジネスロジックがサービスに存在
if (order.getStatus().equals("SHIPPED")) {
throw new Exception("Cannot cancel");
}
order.setStatus("CANCELLED");
}
}
// GOOD: Rich Domain Model
class Order {
private OrderStatus status;
public void cancel() {
// ビジネスロジックがエンティティ内に存在
if (this.status == OrderStatus.SHIPPED) {
throw new OrderDomainException("Cannot cancel shipped order");
}
this.status = OrderStatus.CANCELLED;
}
}
10.3 God Class(神クラス)
1つのクラスが多すぎる責任を持つパターン。SRP違反の極端な形です。
// BAD: 1つのサービスが全てを処理
class OrderGodService {
void createOrder() { /* ... */ }
void cancelOrder() { /* ... */ }
void processPayment() { /* ... */ }
void sendNotification() { /* ... */ }
void generateInvoice() { /* ... */ }
void updateInventory() { /* ... */ }
// ... 数千行のメソッド
}
// GOOD: 責任ごとに分離
class CreateOrderUseCase { /* 注文作成のみ */ }
class CancelOrderUseCase { /* 注文キャンセルのみ */ }
class PaymentService { /* 決済のみ */ }
class NotificationService { /* 通知のみ */ }
10.4 Leaky Abstraction(抽象化の漏れ)
インフラの詳細がドメイン層に漏れるパターン。
// BAD: JPAアノテーションがドメインエンティティに存在
@Entity
@Table(name = "orders")
class Order {
@Id @GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderItem> items;
// JPAというインフラ詳細がドメインに漏出
}
// GOOD: ドメインエンティティとJPAエンティティを分離
// domain/Order.java - 純粋なドメインオブジェクト
class Order {
private final String id;
private List<OrderItem> items;
// フレームワーク依存性なし
}
// infrastructure/OrderJpaEntity.java - JPA専用
@Entity @Table(name = "orders")
class OrderJpaEntity {
@Id private String id;
@OneToMany(cascade = CascadeType.ALL)
private List<OrderItemJpaEntity> items;
}
10.5 Resume-Driven Architecture(履歴書駆動アーキテクチャ)
プロジェクトの要件ではなく、履歴書を華やかにするために過剰な技術を導入するパターン。
「私たちのプロジェクトにはMicroservices + CQRS + Event Sourcing +
Saga Pattern + GraphQL Federationが必要だ!」
実際の要件: ユーザー5人の社内ツール
結果: 6ヶ月後もデプロイできず
原則:問題に合った解決策を選べ。
クイズ
以下
のクイズで
理解度
を
確認
しましょう。
Q1: SOLIDの依存性逆転原則(DIP)の核心は何ですか?
A: 上位モジュール(ビジネスロジック)が下位モジュール(DB、外部サービス)に直接依存せず、両方とも**抽象(インターフェース)**に依存すべきという原則です。これがクリーンアーキテクチャとヘキサゴナルアーキテクチャの核心的な基盤です。
Q2: クリーンアーキテクチャの依存性ルール(Dependency Rule)を説明してください。
A: すべてのソースコード依存性は**内側(上位レベル)**を向く必要があります。Frameworks(外側) -> Adapters -> Use Cases -> Entities(内側)。内側の円は外側の円について何も知りません。
Q3: ヘキサゴナルアーキテクチャでDriving AdapterとDriven Adapterの違いは何ですか?
A: **Driving Adapter(インバウンド)**は外部がアプリケーションを呼び出すアダプターです(REST Controller、CLI、gRPC Handler)。**Driven Adapter(アウトバウンド)**はアプリケーションが外部システムを呼び出すためのアダプターです(DB Repository、HTTP Client、Message Publisher)。入出力の対称性が核心概念です。
Q4: DDDのAggregate設計ルール4つを説明してください。
A:
- Aggregate外部からはRoot Entityのみ参照
- Aggregate間の参照はIDのみ(直接オブジェクト参照禁止)
- 1つのトランザクションで1つのAggregateのみ修正
- Aggregateは**小さく保つ**(1 Root + 少数の内部オブジェクト)
これらのルールはデータの一貫性を保証し、拡張性を確保するためのものです。
Q5: Anemic Domain Modelがアンチパターンである理由は何ですか?
A: Anemic Domain Modelはエンティティにgetter/setterしかなく、ビジネスロジックがサービス層に散らばったパターンです。アンチパターンである理由は:(1)ビジネスルールが複数のサービスに重複/分散して一貫性が崩れやすく、(2)ドメインオブジェクトが単なるデータコンテナに堕してOOPの利点(カプセル化)を失い、(3)ビジネスルール変更時の影響範囲把握が困難になります。Rich Domain Modelではビジネスロジックがエンティティ内に存在し、凝集度が高く変更に安全です。
参考資料
書籍
- Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017)
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)
- Vaughn Vernon, Implementing Domain-Driven Design (2013)
- Vaughn Vernon, Domain-Driven Design Distilled (2016)
- Martin Fowler, Patterns of Enterprise Application Architecture (2002)
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship (2008)
記事・ブログ
- Alistair Cockburn, Hexagonal Architecture — https://alistair.cockburn.us/hexagonal-architecture/
- Robert C. Martin, The Clean Architecture (2012) — https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- Martin Fowler, AnemicDomainModel — https://martinfowler.com/bliki/AnemicDomainModel.html
- Martin Fowler, BoundedContext — https://martinfowler.com/bliki/BoundedContext.html
ツール・フレームワーク
- ArchUnit — https://www.archunit.org/
- Spring Modulith — https://spring.io/projects/spring-modulith
- NestJS CQRS Module — https://docs.nestjs.com/recipes/cqrs
講演・動画
- Uncle Bob, Clean Architecture and Design (NDC Conference)
- Vaughn Vernon, Strategic Domain-Driven Design (InfoQ)
- Alberto Brandolini, Event Storming — https://www.eventstorming.com/
- Netflix Engineering Blog, Domain-Driven Design at Netflix