Table of Contents
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(境界づけられたコンテキスト)は、マイクロサービスの境界と自然にマッピングできました。

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)をインフラから完全に分離できます。

ベンダーロックイン軽減
ドメインロジックがAWS SDKに直接依存しないので、将来的な移行が楽になります。
実践パターン:BaseUseCase / BaseHandler
リクエストフロー

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にイベント収集の仕組みを入れてます。

export abstract class AggregateRoot {
private _domainEvents: DomainEvent[] = [];
protected addDomainEvent(event: DomainEvent): void {
this._domainEvents.push(event);
}
}
EventBridgeでBounded Context間を疎結合にしてます。
段階的に進めた
いきなり全部適用するのは無理だったので、まずはレイヤー分離から始めました。
- Phase 1: Handler / UseCase / Repository の3層分離
- Phase 2: BaseUseCase / BaseHandler の導入
- 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の制約に合わせて適用・簡略化してます。