大規模システム設計
DDDとアーキテクチャパターンで、複雑なシステムの責任分離を実現する
ドメイン駆動設計(DDD)による責任分離
DDDの核心
ドメイン駆動設計は、ビジネスの複雑性を管理するために、ドメインモデルを中心に据えたソフトウェア設計手法です。
DDDの主要概念
- ユビキタス言語:開発者とドメインエキスパートが共有する言語
- 境界づけられたコンテキスト:モデルが適用される明確な境界
- エンティティ:識別子を持つオブジェクト
- 値オブジェクト:不変で識別子を持たないオブジェクト
- 集約:整合性の境界
境界づけられたコンテキスト
コンテキストの分離
大規模システムを、それぞれが独自のモデルを持つ複数のコンテキストに分割します。
// ECサイトの境界づけられたコンテキストの例
// 販売コンテキスト
namespace SalesContext {
interface Product {
id: ProductId;
name: string;
price: Money;
description: string;
}
interface Order {
id: OrderId;
customerId: CustomerId;
items: OrderItem[];
totalAmount: Money;
status: OrderStatus;
}
}
// 在庫コンテキスト
namespace InventoryContext {
interface Product {
id: ProductId;
sku: string;
quantity: number;
location: WarehouseLocation;
}
interface StockMovement {
productId: ProductId;
quantity: number;
type: 'in' | 'out';
timestamp: Date;
}
}
// 配送コンテキスト
namespace ShippingContext {
interface Shipment {
id: ShipmentId;
orderId: OrderId;
items: ShipmentItem[];
destination: Address;
carrier: Carrier;
trackingNumber: string;
}
}
コンテキスト間の統合
- 各コンテキストは独立して進化できる
- 同じ概念(Product)でも、コンテキストによって異なる属性を持つ
- イベント駆動やAPIを通じて連携
レイヤードアーキテクチャ
責任の層別分離
// プレゼンテーション層
// UIに関する責任
class OrderController {
constructor(private orderService: OrderService) {}
async createOrder(req: Request, res: Response) {
try {
const orderData = req.body;
const order = await this.orderService.createOrder(orderData);
res.json({ success: true, orderId: order.id });
} catch (error) {
res.status(400).json({ error: error.message });
}
}
}
// アプリケーション層
// ユースケースの調整責任
class OrderService {
constructor(
private orderRepository: OrderRepository,
private inventoryService: InventoryService,
private paymentService: PaymentService
) {}
async createOrder(orderData: CreateOrderDTO): Promise {
// 在庫確認
await this.inventoryService.checkAvailability(orderData.items);
// ドメインオブジェクトの生成
const order = Order.create(orderData);
// 支払い処理
await this.paymentService.processPayment(order.totalAmount);
// 永続化
await this.orderRepository.save(order);
return order;
}
}
// ドメイン層
// ビジネスロジックの責任
class Order {
private constructor(
private id: OrderId,
private customerId: CustomerId,
private items: OrderItem[],
private status: OrderStatus
) {}
static create(data: CreateOrderData): Order {
if (data.items.length === 0) {
throw new Error('Order must have at least one item');
}
const items = data.items.map(item =>
new OrderItem(item.productId, item.quantity, item.price)
);
return new Order(
OrderId.generate(),
data.customerId,
items,
OrderStatus.PENDING
);
}
get totalAmount(): Money {
return this.items.reduce(
(total, item) => total.add(item.subtotal),
Money.zero()
);
}
confirm(): void {
if (this.status !== OrderStatus.PENDING) {
throw new Error('Only pending orders can be confirmed');
}
this.status = OrderStatus.CONFIRMED;
}
}
// インフラストラクチャ層
// 技術的詳細の責任
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise {
const data = this.mapToDatabase(order);
await db.query(
'INSERT INTO orders (id, customer_id, items, status) VALUES ($1, $2, $3, $4)',
[data.id, data.customerId, JSON.stringify(data.items), data.status]
);
}
private mapToDatabase(order: Order): any {
// ドメインオブジェクトをDBスキーマにマッピング
}
}
ヘキサゴナルアーキテクチャ
ポートとアダプター
ビジネスロジックを中心に据え、外部システムとの接続をポートとアダプターで抽象化します。
// ポート(ビジネスロジックが定義するインターフェース)
// 入力ポート
interface CreateOrderUseCase {
execute(command: CreateOrderCommand): Promise;
}
// 出力ポート
interface OrderRepository {
save(order: Order): Promise;
findById(id: OrderId): Promise;
}
interface PaymentGateway {
charge(amount: Money, paymentMethod: PaymentMethod): Promise;
}
interface NotificationService {
sendOrderConfirmation(email: Email, order: Order): Promise;
}
// ビジネスロジック(ヘキサゴンの中心)
class CreateOrderService implements CreateOrderUseCase {
constructor(
private orderRepo: OrderRepository,
private paymentGateway: PaymentGateway,
private notificationService: NotificationService
) {}
async execute(command: CreateOrderCommand): Promise {
// ビジネスロジックの実行
const order = Order.create(command);
// 支払い処理(出力ポートを通じて)
const paymentResult = await this.paymentGateway.charge(
order.totalAmount,
command.paymentMethod
);
order.confirmPayment(paymentResult);
// 永続化(出力ポートを通じて)
await this.orderRepo.save(order);
// 通知(出力ポートを通じて)
await this.notificationService.sendOrderConfirmation(
command.customerEmail,
order
);
return new OrderCreatedEvent(order);
}
}
// アダプター(外部システムとの接続実装)
// 入力アダプター(REST API)
class OrderRestController {
constructor(private createOrderUseCase: CreateOrderUseCase) {}
async createOrder(req: Request, res: Response) {
const command = this.mapToCommand(req.body);
const event = await this.createOrderUseCase.execute(command);
res.json(this.mapToResponse(event));
}
}
// 出力アダプター(データベース)
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise {
// PostgreSQL固有の実装
}
}
// 出力アダプター(決済サービス)
class StripePaymentGateway implements PaymentGateway {
async charge(amount: Money, paymentMethod: PaymentMethod): Promise {
// Stripe API呼び出し
}
}
マイクロサービスへの適用
サービス境界の設計
責任分離の原則をサービスレベルで適用し、独立してデプロイ可能なサービスを構築します。
サービス分割の指針
- ビジネス機能による分割(機能的凝集)
- データの所有権による分割
- チームの責任範囲による分割
- 変更の頻度による分割
// サービス間通信の例
// 注文サービス
class OrderService {
async createOrder(orderData: CreateOrderDTO) {
// 在庫サービスに在庫確認
const available = await this.inventoryClient
.checkAvailability(orderData.items);
if (!available) {
throw new Error('Insufficient inventory');
}
const order = await this.orderRepository.create(orderData);
// イベントを発行
await this.eventBus.publish(new OrderCreatedEvent({
orderId: order.id,
items: order.items,
customerId: order.customerId
}));
return order;
}
}
// 在庫サービス(イベントを購読)
class InventoryService {
constructor() {
this.eventBus.subscribe('OrderCreated', this.handleOrderCreated);
}
async handleOrderCreated(event: OrderCreatedEvent) {
// 在庫を減らす
for (const item of event.items) {
await this.reduceStock(item.productId, item.quantity);
}
}
}
アーキテクチャ設計の原則
大規模システムでの責任分離
- 高凝集・疎結合を維持する
- 明確な境界を定義する
- 共有データベースを避ける
- 同期的な依存を最小化する
- イベント駆動で疎結合を実現する
- 各サービスは独自のデータストアを持つ