Top View


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

ServerlessでDDD・CQRS・Clean Architectureを試したら相性が良かった

2025/12/21

X (Twitter) シェア

AWS Lambda + TypeScriptでDDD/Clean Architectureを試してみました。

なぜこの組み合わせを試したのか

複数のマイクロサービスをAWS Lambdaで構築してるんですが、ServerlessでDDD/Clean Architectureって相性どうなんだろう?と思って試してみました。

1. ドメインロジックを守りたかった

Lambdaを書いてると「Handlerの中にビジネスロジック直接書いちゃえ」って誘惑がやっぱりあるんですよね。

// こうなりがち
export const handler = async (event) => {
  const dynamodb = new DynamoDBClient({});
  const user = await dynamodb.send(new GetCommand({ TableName, Key }));

  // ビジネスロジックがHandlerに漏れ出す
  if (user.status === 'inactive') {
    throw new Error('User is inactive');
  }
  return { statusCode: 200, body: JSON.stringify(user) };
};

Clean Architectureでレイヤー分離すれば、ビジネスロジックをインフラから守れるんじゃないかと。

2. テストが書きやすくなるはず

UseCase層がRepository Interfaceに依存して、実装詳細(DynamoDB)に依存しない設計にすれば、Lambda全体を起動せずにUseCaseの単体テストが書ける。これは大きい。

3. どこに何を書くか迷わなくなる

「このロジックどこに書けばいい?」って迷いがなくなります。

  • Handler: HTTPリクエスト/レスポンス変換
  • UseCase: ビジネスロジック
  • Domain: エンティティ、値オブジェクト
  • Repository: データ永続化

レイヤーの責務が明確になって、コードレビューで迷うことが減ります。

4. 将来の移行が楽になるかも

ドメインロジックが依存してないので、将来的な移行(DynamoDB -> Aurora Serverlessや新サービス)のハードルが下がります。実際に移行することはないかもしれないけど、「いつでもできる」って安心感はあります。

Serverless × DDD:相性良いと感じた点

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

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

BoundedContext

Aggregate = トランザクション境界

Aggregateはトランザクション境界を定義します。Serverlessでは分散トランザクションが困難なので、「1 Lambda = 1 Aggregate操作」という制約が自然とFat Lambda回避につながってる感じがします。

Event-Driven Architectureとの親和性

DDDのDomain Eventsは、EventBridge/SNS/SQSによるイベント駆動アーキテクチャと相性良いです。Bounded Context間の通信にイベント使うことで疎結合にできる。

Serverless × Clean Architecture:これも相性良い

Hexagonal Architecture(Ports & Adapters)との相性

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

Hexagonal

ベンダーロックイン軽減

ドメインロジックがAWS SDKに直接依存しないので、将来的な移行が楽になります。

実践パターン:BaseUseCase / BaseHandler

リクエストフロー

RequestFlow

BaseUseCase

export abstract class BaseUseCase<TRequest, TResponse> {
  abstract execute(request: TRequest): Promise<TResponse>;
}

各UseCaseはこれを継承して、executeメソッドを実装します。

BaseHandler

export abstract class BaseHandler<TRequest, TResponse> {
  public handler = async (event: APIGatewayProxyEvent) => {
    const request = this.parseRequest(event);      // 1. パース
    const result = await this.execute(request);     // 2. UseCase実行
    return this.toResponse(result);                 // 3. レスポンス変換
  };

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

Template Methodパターンで共通処理を集約してます。

CQRS

読み取り(Query)と書き込み(Command)で関心事を分離してます。

  • Query(読み取り):キャッシュ可能、副作用なし、パフォーマンス最適化しやすい
  • Command(書き込み):トランザクション管理、イベント発行が必要

Serverlessだと読み取りと書き込みでLambdaを分けやすいので、CQRSと合う。

Domain Events

Aggregate Rootにイベント収集の仕組みを入れてます。

DomainEvents

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

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

EventBridgeでBounded Context間を疎結合にしてます。

段階的に進めた

いきなり全部適用するのは無理だったので、まずはレイヤー分離から始めました。

  1. Phase 1: Handler / UseCase / Repository の3層分離
  2. Phase 2: BaseUseCase / BaseHandler の導入
  3. Phase 3: Domain層の充実(Entity、Value Object)

まとめ

観点Serverlessの特性DDD/CAでの対処
トランザクション分散、ACIDなしSaga、Eventual Consistency
関数粒度Nano vs Monolithモジュラーアプローチ
コネクションステートレスRDS Proxy、DynamoDB推奨
テスタビリティインフラ依存Ports & Adapters

DDDもClean Architectureも手段でしかないので、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