Top View


Author shiro seike / せいけ しろう / 清家 史郎

AWS Lambda × TypeScriptで実践するDDD・Clean Architecture・CQRS — Serverlessマイクロサービスの設計パターン

2025/12/21

X (Twitter) シェア

はじめに

AWS Lambdaを中心としたServerlessアーキテクチャで複数のマイクロサービスを構築・運用する中で、ドメイン駆動設計(DDD)、Clean Architecture、CQRSの組み合わせが非常に有効であることを実感しています。

CQRS+ESやClean Architectureは大規模システム専用の設計パターンと見なされがちですが、AWS Lambda + TypeScriptの組み合わせでは、Serverlessの特性を活かしながら段階的に導入できることがわかりました。

本記事では、Serverless環境におけるDDD/Clean Architecture/CQRSの実践パターンを紹介します。 AWS Lambda、Amazon DynamoDB、Amazon EventBridgeを中心としたアーキテクチャにおいて、どのように設計パターンを適用し、ビジネス価値向上に繋がるかを解説します。

なぜServerless × DDD/Clean Architectureなのか

1. ドメインロジックをインフラストラクチャから分離する

AWS Lambdaでアプリケーションを構築する際、Handler関数にビジネスロジックが密結合する傾向があります。以下のようなコードは多くのプロジェクトで見かけるパターンです。

// Handler内にバリデーション・ビジネスロジック・DB操作が混在
export const handler = async (event: APIGatewayProxyEvent) => {
  const body = JSON.parse(event.body || '{}');
  // バリデーション、ビジネスロジック、DB操作が全部ここに...
  const result = await docClient.send(new PutCommand({
    TableName: process.env.TABLE_NAME,
    Item: { pk: `ORDER#${orderId}`, sk: `ORDER#${orderId}`, ...body },
  }));
  return { statusCode: 201, body: JSON.stringify(result) };
};

小規模なうちは問題になりませんが、複数の顧客プロジェクトを並行して開発・保守するようになると、この密結合がメンテナンスコストの増大を招きます。

Clean ArchitectureのPorts & Adaptersパターンを適用することで、Lambda HandlerをPrimary Adapter、Amazon DynamoDBや外部APIをSecondary Adapterとして位置づけられます。

ドメインロジックをAWS SDKへの依存から完全に切り離せます。

┌─────────────────────────────────────────────────┐
│  Lambda Handler (Primary Adapter)                │
│    ↓                                             │
│  ┌─────────────────────────────────────────┐     │
│  │  UseCase (Application Service)          │     │
│  │    ↓                                    │     │
│  │  ┌───────────────────────────────┐      │     │
│  │  │  Domain (Entity, Value Object)│      │     │
│  │  └───────────────────────────────┘      │     │
│  │    ↓                                    │     │
│  │  Repository Interface (Port)            │     │
│  └─────────────────────────────────────────┘     │
│    ↓                                             │
│  DynamoDB Repository (Secondary Adapter)         │
└─────────────────────────────────────────────────┘

この分離により、ドメインロジックの再利用性が飛躍的に向上します。

実際に、あるプロジェクトでDynamoDBからAmazon Aurora Serverless v2への移行が発生した際、Repository Interfaceの実装を差し替えるだけで、UseCase層以上のコードは一切変更せずに移行を完了できました。

インフラストラクチャの変更がドメインロジックに影響しないという設計上の利点を、実プロジェクトで実証した形です。

2. テスタビリティの向上によるリリースサイクルの短縮

UseCase層がRepository Interfaceに依存する設計にすることで、Lambda実行環境を起動せずにUseCaseの単体テストが実行可能になります。

// テスト時はRepository Interfaceのモック実装を注入
const mockRepo: OrderRepository = {
  findById: async (id: OrderId) => createTestOrder(id),
  save: async (order: Order) => { /* mock */ },
};

const useCase = new CreateOrderUseCase(mockRepo);
const result = await useCase.execute(createOrderRequest);
expect(result.status).toBe('CREATED');

この設計を導入したプロジェクトでは、ユニットテストのカバレッジが大幅に向上し、CI/CDパイプラインにおけるテスト実行時間もLambda統合テスト中心のアプローチと比較して大きく削減されました。

テストの高速化により、1日あたりのデプロイ頻度が増加し、顧客への機能提供速度が改善しています。

3. 開発チームのコード品質の標準化

「このロジックはどのレイヤーに書くべきか」という判断が明確になります。

レイヤー責務AWS対応
HandlerHTTPリクエスト/レスポンス変換API Gateway + Lambda Handler
UseCaseビジネスロジックの実行純粋なTypeScript
Domainエンティティ、値オブジェクト、ドメインイベント純粋なTypeScript
Repositoryデータ永続化DynamoDB SDK / Aurora Data API

レイヤーの責務が明確になることで、コードレビューの指摘が設計方針の議論から実装品質のレビューに変化しました。

新しいメンバーのオンボーディングでも、「どこに何を書くか」という最初のハードルが大幅に低減しています。

4. 将来の移行可能性

ドメインロジックがAWS SDKに直接依存しないため、将来的な移行(DynamoDB → Aurora Serverless、Lambda → ECS Fargate、あるいは新サービスへの対応)のハードルが低くなります。

移行の必要が生じた際に、ドメインロジックを変更せずにインフラストラクチャ層のみを差し替えられる設計は、長期的なプロダクト運用において大きな価値を持ちます。

Serverless × DDD: 設計上の親和性

Bounded Context = マイクロサービス境界

DDDのBounded Context(境界づけられたコンテキスト)は、Serverlessマイクロサービスの境界と自然にマッピングできます。

たとえばECシステムでは以下のようにBounded Contextを分割しました。

  • 注文コンテキスト: 注文のライフサイクル管理(Lambda × API Gateway × DynamoDB)
  • 在庫コンテキスト: 在庫の引き当て・管理(Lambda × DynamoDB Streams)
  • 通知コンテキスト: メール/Push通知(Lambda × Amazon SES × Amazon SNS)

BoundedContext

各コンテキストは独立したAWS CDKスタックとしてデプロイされ、チームごとに独立したリリースサイクルを実現しています。

Aggregate = トランザクション境界とFat Lambda回避

DDDのAggregateはトランザクション境界を定義します。

Serverlessでは分散トランザクションが困難なので、「1 Lambda = 1 Aggregate操作」という制約を設けると、自然とFat Lambda(1つのLambdaに過剰な責務を持たせるアンチパターン)を回避できます。

DynamoDBのトランザクション機能(TransactWriteItems)をAggregate内の整合性保証に活用し、Aggregate間はEventual Consistencyで整合性を担保しています。

Domain EventsとAmazon EventBridgeの組み合わせ

DDDのDomain Eventsは、Amazon EventBridgeによるイベント駆動アーキテクチャと極めて親和性が高いです。Bounded Context間の通信にイベントを使うことで疎結合な連携を実現できます。

DomainEvents

export abstract class AggregateRoot {
  private _domainEvents: DomainEvent[] = [];

  protected addDomainEvent(event: DomainEvent): void {
    this._domainEvents.push(event);
  }

  get domainEvents(): ReadonlyArray<DomainEvent> {
    return this._domainEvents;
  }

  clearEvents(): void {
    this._domainEvents = [];
  }
}

Aggregate Rootに蓄積されたDomain Eventsは、UseCase実行後にEventBridgeへPublishされます。

たとえば「注文確定」イベントが発行されると:

  1. 在庫コンテキストが在庫を引き当て
  2. 通知コンテキストが確認メールを送信
  3. 分析コンテキストが売上データを集計

といった処理が、各コンテキストの独立したLambdaで非同期に実行されます。新たなコンテキストの追加もEventBridgeのルールを追加するだけで対応可能です。

Serverless × Clean Architecture: Hexagonal Architecture

Lambda HandlerはPrimary Adapter、DynamoDB/外部APIはSecondary Adapter。ビジネスロジック(Domain)をインフラから完全に分離できます。

Hexagonal

Serverless Land(serverlessland.com)でもHexagonal Architecture(Ports & Adapters)はLambdaのベストプラクティスとして推奨されています。

大規模システム専用ではなく、Lambda1つから段階的に導入できるアーキテクチャパターンです。

ドメインロジックがAWS SDKに直接依存しないため、ベンダーロックインの懸念も軽減されます。

実践パターン: BaseUseCase / BaseHandler

RequestFlow

BaseUseCase

各UseCaseが継承する抽象クラスにより、横断的関心事(ロギング、エラーハンドリング、Amazon CloudWatch Metricsへのカスタムメトリクス出力)を統一的に管理します。

export abstract class BaseUseCase<TRequest, TResponse> {
  constructor(
    protected readonly logger: Logger,
    protected readonly metrics: MetricsPublisher,
  ) {}

  async run(request: TRequest): Promise<TResponse> {
    const startTime = Date.now();
    try {
      this.logger.info('UseCase started', { useCase: this.constructor.name });
      const result = await this.execute(request);
      this.metrics.putMetric('UseCaseSuccess', 1);
      return result;
    } catch (error) {
      this.metrics.putMetric('UseCaseError', 1);
      this.logger.error('UseCase failed', { error });
      throw error;
    } finally {
      this.metrics.putMetric('UseCaseDuration', Date.now() - startTime);
    }
  }

  protected abstract execute(request: TRequest): Promise<TResponse>;
}

BaseHandler(Template Methodパターン)

Lambda HandlerのボイラープレートをTemplate Methodパターンで集約しています。

export abstract class BaseHandler<TRequest, TResponse> {
  public handler = async (
    event: APIGatewayProxyEvent
  ): Promise<APIGatewayProxyResult> => {
    try {
      const request = this.parseRequest(event);   // 1. リクエストパース
      const result = await this.execute(request);  // 2. UseCase実行
      return this.toResponse(result);              // 3. レスポンス変換
    } catch (error) {
      return this.handleError(error);              // 4. エラーハンドリング
    }
  };

  protected abstract parseRequest(event: APIGatewayProxyEvent): TRequest;
  protected abstract execute(request: TRequest): Promise<TResponse>;
  protected abstract toResponse(result: TResponse): APIGatewayProxyResult;
  protected abstract handleError(error: unknown): APIGatewayProxyResult;
}

この共通基盤により、新しいエンドポイントの追加は parseRequest / execute / toResponse の3メソッドを実装するだけ で完了します。ボイラープレートの削減と品質の統一を同時に実現できる設計です。

CQRS: 読み取りと書き込みの分離

CQRSパターンはServerlessアーキテクチャと特に相性が良い設計パターンです。

  • Query(読み取り): Amazon DynamoDBのGSIを活用し、読み取り専用Lambda + Amazon CloudFront + API Gatewayキャッシュでパフォーマンスを最適化。副作用がないため、Lambda Concurrencyの制限も緩和しやすい
  • Command(書き込み): トランザクション管理、Aggregate整合性の検証、Domain Event発行を行う。DynamoDB TransactWriteItems で原子性を保証

CQRS Architecture

Serverlessでは読み取りと書き込みのLambdaを物理的に分離できるため、それぞれ独立してスケーリングとコスト最適化が可能です。

実際のプロジェクトでは、読み取り系の呼び出しが書き込み系の約10倍あるケースにおいて、Query Lambdaのメモリ設定を最小化し、Command Lambdaには十分なメモリを割り当てることで、全体のLambdaコストを最適化できました。

CQRSで分離しているからこそ、こうした細かいチューニングが容易になります。

2024年のCQRS+ESカンファレンスをきっかけにAWS Lambda + DynamoDBでCQRSパターンを検証した結果、SQSやEventBridgeを別途用意しなくてもDynamoDB Streamsだけでイベント駆動が実現できることがわかりました。

追加のインフラコストなしにCQRSの恩恵を得られる点は、Serverlessアーキテクチャの大きな強みです。

段階的な導入アプローチ

すべてのパターンを一度に適用するのではなく、既存のチーム体制やプロジェクトの状況に合わせて段階的に導入しました。

Phase 1: レイヤー分離(2〜3日)

Handler / UseCase / Repository の3層に分離。これだけでもテスタビリティが大幅に向上します。

Phase 2: 共通基盤の導入(1〜2週間)

BaseUseCase / BaseHandler を導入し、ロギング・メトリクス・エラーハンドリングを統一。Amazon CloudWatch Metricsへのカスタムメトリクス出力も基盤に組み込みます。

Phase 3: Domain層の充実(継続的)

Entity、Value Object、Domain Eventを段階的に導入。既存コードのリファクタリングと並行して進めます。

この段階的アプローチにより、既存のプロダクション環境を止めることなく、アーキテクチャの改善を継続的に進めることができました。

まとめ: Serverlessの制約をDDD/Clean Architectureで制す

観点Serverlessの特性DDD/CAでの対処活用AWSサービス
トランザクション分散、ACIDなしSaga、Eventual ConsistencyEventBridge, DynamoDB Transactions
関数粒度Fat Lambda vs Nano Services1 Lambda = 1 Aggregate操作Lambda, API Gateway
コネクションステートレス、コールドスタートRepository抽象化DynamoDB, RDS Proxy
テスタビリティインフラ依存が強いPorts & AdaptersCDK (IaC), CloudWatch
スケーラビリティ読み書きが異なる負荷CQRS分離Lambda Concurrency, CloudFront
サービス間連携同期呼び出しは脆いDomain EventsEventBridge, SNS, SQS

DDD、Clean Architecture、CQRSはいずれも手段であり、目的ではありません。

Serverlessの制約(ステートレス、分散、イベント駆動)に合わせて柔軟に適用・簡略化することが重要です。

一方で、これらのパターンを適切に適用することで、顧客のプロダクトの保守性・拡張性・テスタビリティが劇的に向上することを、複数のプロジェクトで実証してきました。

AWSのServerlessサービス群は、これらの設計パターンを実践する上で非常に強力な基盤を提供してくれます。

Phase 1のレイヤー分離だけでもテスタビリティと保守性に大きな効果があるため、Serverlessプロジェクトへの導入を検討されている方は、まずここから始めることをお勧めします。

shiro seike / せいけ しろう / 清家 史郎

shiro seike / せいけ しろう / 清家 史郎

Twitter X

Company:Fusic CO., LTD. Slides:slide.seike460.com blog:blog.seike460.com Program Language:PHP , Go Interest:Full Serverless Architecture