👋

NestJSによるモジュラーモノリスの実装 - 複数ベンダーでの協業開発

に公開

はじめに

システム開発において、複数の企業(ベンダー)が共同して一つのプロジェクトに取り組むケースは珍しくありません。従来のモノリシックアーキテクチャでは責任範囲が曖昧になりがちで、マイクロサービスでは分割の粒度やサービス間連携の複雑さに悩まされることがあります。

そこで注目したいのが「モジュラーモノリス」というアプローチです。本記事では、NestJSを使ったモジュラーモノリスの実装方法を解説し、特に以下のポイントに焦点を当てます:

  • 複数ベンダーが同時並行で開発できる構成
  • インターフェースによる機能の公開・非公開の制御
  • モジュール間の通信方法(API/メッセージング)

モジュラーモノリスとは

モジュラーモノリスとは、単一のデプロイ単位として管理できる(モノリシックな)アプリケーションでありながら、内部的には明確に分離されたモジュールで構成される設計パターンです。

モノリスとマイクロサービスの中間に位置する選択肢

モジュラーモノリスは以下のような特徴を持ちます:

  • 単一のコードベース: 全てのモジュールが単一のリポジトリで管理される
  • 明確なモジュール境界: 各モジュールは明確なAPIを通じてのみ通信
  • 独立した開発: モジュールごとに異なるチーム(企業)が担当可能
  • デプロイの柔軟性: 単一ユニットまたは個別のコンテナとしてデプロイ可能

この特性により、モノリスの開発シンプルさとマイクロサービスの柔軟性の良いところを組み合わせることができます。

従来のアーキテクチャとの比較

特性 モノリス マイクロサービス モジュラーモノリス
開発の複雑さ 低い 高い 中程度
デプロイ 単一ユニット 複数独立サービス 柔軟(単一/複数選択可)
チーム間の連携 高結合 低結合 中程度の結合
スケーリング 全体単位 サービス単位 モジュール単位可能
初期オーバーヘッド 低い 高い 中程度
責任境界 曖昧になりがち 明確 明確

NestJSとモジュラーモノリス

NestJSはモジュラーモノリスの実装に最適なフレームワークの一つです。その理由は:

  1. モジュール指向: NestJSはモジュールを基本単位とする設計
  2. DI(依存性注入): インターフェースを用いた疎結合な設計が容易
  3. スケーラビリティ: モノリスからマイクロサービスへの移行も容易

NestJSのモジュール構造活用

NestJSのモジュールは以下の特性を持ち、モジュラーモノリスに適しています:

// NestJSのモジュール定義例
@Module({
  imports: [依存モジュール],    // 他モジュールへの依存
  controllers: [コントローラー], // API エンドポイント
  providers: [サービス],        // ビジネスロジック
  exports: [外部公開要素]       // 他モジュールに公開する機能
})
export class SomeModule {}

exports を適切に設定することで、モジュール間の依存関係と公開範囲を明確に制御できます。

複数ベンダーによる協業開発の実現

プロジェクト構成

複数ベンダーで開発するプロジェクトの基本構成例を示します:

project-root/
├── src/
│   ├── main.ts                     # メインエントリーポイント
│   ├── app.module.ts               # ルートモジュール
│   │
│   ├── shared/                     # 共通モジュール(共同管理)
│   │   ├── shared.module.ts
│   │   ├── interfaces/             # 共通インターフェース
│   │   └── infrastructure/         # 共通インフラ(DB接続など)
│   │
│   ├── vendor-a/                   # A社が担当するモジュール
│   │   ├── README.md               # A社モジュールのドキュメント
│   │   ├── vendor-a.module.ts      # A社モジュールのエントリーポイント
│   │   ├── main.ts                 # 単独コンテナ用エントリーポイント
│   │   ├── domain/                 # A社のドメイン層
│   │   │   ├── entities/           # ドメインエンティティ
│   │   │   ├── value-objects/      # 値オブジェクト
│   │   │   └── repositories/       # リポジトリインターフェース
│   │   │
│   │   ├── application/            # A社のアプリケーション層
│   │   │   ├── dtos/               # DTOs
│   │   │   ├── services/           # アプリケーションサービス
│   │   │   └── events/             # イベント定義
│   │   │
│   │   ├── infrastructure/         # A社のインフラ層
│   │   │   ├── database/           # DB関連実装
│   │   │   └── external-services/  # 外部サービス連携
│   │   │
│   │   └── presentation/           # A社のプレゼンテーション層
│   │       ├── controllers/        # APIコントローラー
│   │       └── filters/            # 例外フィルターなど
│   │
│   ├── vendor-b/                   # B社が担当するモジュール
│   │   ├── vendor-b.module.ts
│   │   ├── main.ts                 # 単独コンテナ用エントリーポイント
│   │   └── ...                     # 同様の構造
│   │
│   └── vendor-c/                   # C社が担当するモジュール
│       └── ...                     # 同様の構造
│
├── test/                           # テストファイル
│   ├── vendor-a/                   # A社モジュールのテスト
│   ├── vendor-b/                   # B社モジュールのテスト
│   └── integration/                # 結合テスト
│
├── dockerfile.vendor-a             # A社モジュール用Dockerfile
├── dockerfile.vendor-b             # B社モジュール用Dockerfile
├── docker-compose.yml              # 開発/本番環境設定
└── package.json

責任範囲の明確化

複数ベンダーで効率的に開発を進めるためには、責任範囲を明確にすることが重要です:

  1. モジュール所有権の明確化:

    • 各ベンダーは担当するモジュールフォルダ内のコードに責任を持つ
    • コード変更やPRレビューの責任所在が明確になる
  2. インターフェース契約の管理:

    • 共通インターフェース定義は共同で管理
    • 変更提案はすべての関係者でレビュー
    • バージョニングやリリースプランを明確に
  3. コーディング規約と品質基準:

    • 共通のコーディング規約を確立
    • 各ベンダーのコードはその規約に従うべき
    • 自動化されたコード品質チェックの導入

チーム間のコラボレーション戦略

  1. 明確なコミュニケーションチャネル:

    • モジュール間の依存関係や変更がある場合の連絡方法
    • 定期的な進捗共有とレビュー
  2. ドキュメント管理:

    • 各モジュールには README.md で基本情報を記載
    • 公開インターフェースの使用方法を明確に文書化
    • API仕様書の自動生成と公開
  3. 共同作業のワークフロー:

    • ブランチ戦略(feature/vendor-a/xxx など)
    • PRテンプレートとレビュープロセス
    • CIパイプラインによる自動テスト

協業開発の実際の例

A社とB社の協業開発の流れを例示します:

  1. 初期設計フェーズ:

    • 共通インターフェースの設計
    • モジュール間の依存関係の決定
    • 共通データモデルの定義
  2. 並行開発フェーズ:

    • A社: 商品管理モジュール開発
    • B社: 注文管理モジュール開発
    • 各社は自社モジュール内での実装に集中
  3. 統合フェーズ:

    • モジュール間の統合テスト
    • パフォーマンステスト
    • 相互依存の問題解決

インターフェースによる公開・非公開の制御

モジュラーモノリスの核となるのが、適切に設計されたインターフェースです。インターフェースを通じて各モジュールは公開する機能と非公開にする機能を明確に区別できます。

公開インターフェースの設計原則

インターフェースを設計する際の重要な原則:

  1. 最小公開の原則: 必要な機能だけを公開する
  2. 安定性: 頻繁に変更されないインターフェースを設計する
  3. ドメイン境界の尊重: 他モジュールのドメインロジックに干渉しない
  4. 明確な契約: 入出力と期待される動作を明確に定義する

公開インターフェースの例

// shared/interfaces/product-read.interface.ts
export interface ProductReadData {
  id: string;
  name: string;
  price: number;
  isAvailable: boolean;
}

// 読み出し専用インターフェース
export interface ProductReadOnlyRepositoryInterface {
  findById(id: string): Promise<ProductReadData | null>;
  findAll(criteria?: any): Promise<ProductReadData[]>;
  findByCategory(categoryId: string): Promise<ProductReadData[]>;
}

// 書き込み操作を含む完全なインターフェース(内部利用のみ)
export interface ProductRepositoryInterface extends ProductReadOnlyRepositoryInterface {
  save(product: ProductWriteData): Promise<ProductReadData>;
  update(id: string, data: Partial<ProductWriteData>): Promise<ProductReadData>;
  delete(id: string): Promise<boolean>;
}

インターフェースの実装と公開

// vendor-a/vendor-a.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([ProductModel]),
  ],
  providers: [
    // 内部用の完全なリポジトリ
    {
      provide: 'ProductRepository',
      useClass: ProductRepository,
    },
    // 外部公開用の読み出し専用リポジトリ
    {
      provide: 'ProductReadOnlyRepository',
      useClass: ProductReadRepository,
    },
  ],
  exports: [
    'ProductReadOnlyRepository', // 読み出し専用インターフェースのみを公開
  ],
})
export class VendorAModule {}

他社からのインターフェース利用

他のベンダーは公開されたインターフェースのみにアクセスでき、内部実装の詳細は隠蔽されます:

// vendor-b/services/order.service.ts
@Injectable()
export class OrderService {
  constructor(
    @Inject('ProductReadOnlyRepository')
    private productReadRepository: ProductReadOnlyRepositoryInterface
  ) {}

  async getOrderWithProducts(orderId: string) {
    // A社の公開インターフェースを利用して製品情報を取得
    const products = await this.productReadRepository.findByIds(
      this.getProductIdsFromOrder(orderId)
    );
    
    // 以降はB社独自のロジック
  }
}

インターフェース設計のベストプラクティス

  1. ユースケース中心設計: どのような操作が必要かを明確にしてから設計
  2. 粗粒度のインターフェース: 細かすぎる操作より、ユースケースに合った操作を提供
  3. バージョニング考慮: 将来の変更に対応しやすい設計
  4. 例外処理の明確化: エラー状態と例外処理も契約の一部として設計

## モジュール間通信の実装

モジュラーモノリスでは、モジュール間の通信方法として主に2つのアプローチがあります:

1. **APIによる同期通信**: RESTful API、GraphQLなど
2. **メッセージングによる非同期通信**: イベント、コマンド、クエリなど

### APIによる通信

#### 単一プロセスでの内部API呼び出し

単一プロセスで実行する場合、NestJSの依存性注入(DI)システムを使って他モジュールのサービスを直接呼び出せます:

```typescript
// vendor-b/services/order.service.ts
@Injectable()
export class OrderService {
  constructor(
    private productService: ProductService, // A社のサービス
  ) {}

  async createOrder(userId: string, productIds: string[]) {
    // A社のサービスAPIを呼び出し
    const products = await this.productService.getProductsByIds(productIds);
    
    // B社のロジック
  }
}

分散環境でのHTTP API呼び出し

別コンテナで実行する場合、HTTPクライアントを使って外部API呼び出しを行います:

// vendor-b/infrastructure/product-client.ts
@Injectable()
export class ProductClient implements ProductReadOnlyRepositoryInterface {
  constructor(private httpService: HttpService) {}
  
  private apiUrl = process.env.VENDOR_A_API_URL || 'http://vendor-a-service:3000';

  async findById(id: string): Promise<ProductReadData | null> {
    try {
      const response = await this.httpService.get(
        `${this.apiUrl}/products/${id}`
      ).toPromise();
      return response.data;
    } catch (error) {
      if (error.response?.status === 404) return null;
      throw error;
    }
  }
  
  // 他のメソッド実装...
}

// vendor-b/modules/vendor-b.module.ts
@Module({
  imports: [
    HttpModule,
    // 環境によって異なるモジュールを読み込む
    process.env.DEPLOYMENT_MODE === 'standalone' 
      ? VendorAModule // 単一プロセスの場合
      : [], // 分散環境の場合は不要
  ],
  providers: [
    OrderService,
    // 環境によって適切な実装を提供
    {
      provide: 'ProductReadOnlyRepository',
      useClass: process.env.DEPLOYMENT_MODE === 'standalone' 
        ? ProductReadRepository // 単一プロセスの場合
        : ProductClient // 分散環境の場合
    }
  ]
})
export class VendorBModule {}

メッセージングによる通信

イベント駆動アーキテクチャを採用することで、モジュール間の結合をさらに低減できます。NestJSのイベントエミッターや外部メッセージブローカー(RabbitMQ, Kafkaなど)を利用します。

イベント定義

// shared/events/product-events.ts
export class ProductStockUpdatedEvent {
  constructor(
    public readonly productId: string,
    public readonly newQuantity: number,
    public readonly timestamp: Date = new Date()
  ) {}
}

イベント発行(A社)

// vendor-a/services/product.service.ts
@Injectable()
export class ProductService {
  constructor(
    private eventEmitter: EventEmitter2,
    @InjectRepository(ProductModel)
    private productRepository: Repository<ProductModel>
  ) {}

  async updateStock(productId: string, quantity: number) {
    // 在庫更新ロジック
    const product = await this.productRepository.findOne({ where: { id: productId } });
    if (!product) {
      throw new Error('Product not found');
    }
    
    const newQuantity = product.stockQuantity + quantity;
    if (newQuantity < 0) {
      throw new Error('Insufficient stock');
    }
    
    // DBを更新
    await this.productRepository.update(productId, { stockQuantity: newQuantity });
    
    // イベント発行
    this.eventEmitter.emit(
      'product.stock.updated',
      new ProductStockUpdatedEvent(productId, newQuantity)
    );
    
    return { id: productId, newQuantity };
  }
}

イベント購読(B社)

// vendor-b/listeners/product-listener.ts
@Injectable()
export class ProductEventListener {
  constructor(
    @InjectRepository(OrderModel)
    private orderRepository: Repository<OrderModel>
  ) {}

  @OnEvent('product.stock.updated')
  async handleProductStockUpdated(event: ProductStockUpdatedEvent) {
    // B社側での在庫更新イベントの処理
    console.log(`在庫更新: ${event.productId}, 新数量: ${event.newQuantity}`);
    
    // 例: 関連する注文の状態更新
    if (event.newQuantity === 0) {
      // 在庫切れになった商品を含む未処理の注文を検索
      const affectedOrders = await this.orderRepository.find({
        where: {
          status: 'pending',
          productIds: Like(`%${event.productId}%`) // 簡易実装
        }
      });
      
      // 注文状態の更新などの処理
      // ...
    }
  }
}

外部メッセージブローカーを使用した実装

大規模な環境や分散デプロイでは、RabbitMQやKafkaなどの外部メッセージブローカーを使用します:

// main.ts (共通設定)
import { MicroserviceOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // マイクロサービス接続を追加
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.RMQ,
    options: {
      urls: [process.env.RABBITMQ_URL || 'amqp://localhost:5672'],
      queue: 'products_queue',
      queueOptions: { durable: true },
    },
  });
  
  // 両方のサーバーを起動
  await app.startAllMicroservices();
  await app.listen(3000);
}
// vendor-a/services/product.service.ts
@Injectable()
export class ProductService {
  constructor(
    private client: ClientProxy,
    // ...
  ) {}

  async updateStock(productId: string, quantity: number) {
    // ...データベース更新処理...
    
    // メッセージブローカー経由でイベント発行
    this.client.emit<any>(
      'product.stock.updated', 
      new ProductStockUpdatedEvent(productId, newQuantity)
    );
  }
}

// vendor-b/listeners/product-listener.ts
@Controller()
export class ProductEventListener {
  // ...
  
  @EventPattern('product.stock.updated')
  async handleProductStockUpdated(data: ProductStockUpdatedEvent) {
    // イベント処理ロジック
  }
}

実装例: 読み出し専用リポジトリの公開

A社が保有する商品データをB社が参照するケースを考えてみましょう。A社は書き込み操作を制限し、読み出し操作のみを公開します。

1. 共通インターフェースの定義

// shared/interfaces/product-repository.interface.ts
export interface ProductReadData {
  id: string;
  name: string;
  price: number;
  isAvailable: boolean;
}

export interface ProductReadOnlyRepositoryInterface {
  findById(id: string): Promise<ProductReadData | null>;
  findAll(criteria?: any): Promise<ProductReadData[]>;
  findByCategory(categoryId: string): Promise<ProductReadData[]>;
}

2. A社モジュールでのインターフェース実装

// vendor-a/infrastructure/product-read-repository.ts
@Injectable()
export class ProductReadRepository implements ProductReadOnlyRepositoryInterface {
  constructor(
    @InjectRepository(ProductModel)
    private productModel: Repository<ProductModel>,
  ) {}

  async findById(id: string): Promise<ProductReadData | null> {
    const product = await this.productModel.findOne({ where: { id } });
    if (!product) return null;
    return this.toReadData(product);
  }

  async findAll(criteria?: any): Promise<ProductReadData[]> {
    const products = await this.productModel.find(criteria);
    return products.map(p => this.toReadData(p));
  }

  async findByCategory(categoryId: string): Promise<ProductReadData[]> {
    const products = await this.productModel.find({ 
      where: { categoryId } 
    });
    return products.map(p => this.toReadData(p));
  }

  private toReadData(model: ProductModel): ProductReadData {
    return {
      id: model.id,
      name: model.name,
      price: model.price,
      isAvailable: model.stockQuantity > 0
    };
  }
}

3. A社モジュールでの公開設定

// vendor-a/vendor-a.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([ProductModel]),
  ],
  providers: [
    // 内部用の完全なリポジトリ(書き込み可能)
    {
      provide: 'ProductRepository',
      useClass: ProductRepository,
    },
    // 外部公開用の読み出し専用リポジトリ
    {
      provide: 'ProductReadOnlyRepository',
      useClass: ProductReadRepository,
    },
  ],
  exports: [
    'ProductReadOnlyRepository', // 読み出し専用インターフェースだけを公開
  ],
})
export class VendorAModule {}

4. B社モジュールでのインターフェース利用

// vendor-b/services/order.service.ts
@Injectable()
export class OrderService {
  constructor(
    @InjectRepository(OrderModel)
    private orderRepository: Repository<OrderModel>,
    
    @Inject('ProductReadOnlyRepository')
    private productReadRepository: ProductReadOnlyRepositoryInterface
  ) {}

  async getOrderWithProducts(orderId: string) {
    const order = await this.orderRepository.findOne({ where: { id: orderId } });
    if (!order) {
      throw new Error('Order not found');
    }
    
    // A社の読み出し専用リポジトリを利用
    const products = await Promise.all(
      order.productIds.map(id => this.productReadRepository.findById(id))
    );
    
    return {
      id: order.id,
      totalAmount: order.totalAmount,
      products: products.filter(p => p !== null),
    };
  }
  
  // 注文作成時は製品データを参照のみ(更新は別インターフェースを使用)
  async createOrder(userId: string, productIds: string[], quantities: number[]) {
    // 製品情報を読み取り
    const productPromises = productIds.map(id => this.productReadRepository.findById(id));
    const products = await Promise.all(productPromises);
    
    // 存在確認
    const missingProducts = productIds.filter((id, index) => !products[index]);
    if (missingProducts.length > 0) {
      throw new Error(`Products not found: ${missingProducts.join(', ')}`);
    }
    
    // 利用可能確認
    const unavailableProducts = products.filter(p => !p.isAvailable);
    if (unavailableProducts.length > 0) {
      throw new Error(`Some products are not available`);
    }
    
    // 合計金額計算
    const totalAmount = products.reduce((sum, product, index) => {
      return sum + (product.price * quantities[index]);
    }, 0);
    
    // 注文作成
    const order = {
      id: crypto.randomUUID(),
      userId,
      productIds,
      quantities,
      totalAmount,
      status: 'created',
      createdAt: new Date(),
    };
    
    await this.orderRepository.save(order);
    
    // 在庫更新は別のサービス(API)を通じて行う
    // await this.productStockService.updateStock(productIds, quantities);
    
    return order;
  }
}

開発とテストの効率化

Docker Composeによる統合環境

モジュラーモノリスでは、Docker Composeを活用することで、開発やテスト時の統合環境の構築が容易になります。各ベンダーが独自に開発を進めつつも、定期的に結合テストを行うことが可能です:

# docker-compose.dev.yml
version: '3.8'

services:
  app-dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
      - ./test:/app/test
    environment:
      - NODE_ENV=development
    command: npm run start:dev
    depends_on:
      - db-dev

  db-dev:
    image: postgres:14
    volumes:
      - postgres_data_dev:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_USER: user
      POSTGRES_DB: app_dev
    ports:
      - "5432:5432"

volumes:
  postgres_data_dev:

結合テスト環境

各モジュールの統合テストも簡単に構成できます:

# docker-compose.test.yml
version: '3.8'

services:
  vendor-a-test:
    build:
      context: .
      dockerfile: dockerfile.vendor-a.test
    environment:
      - NODE_ENV=test
      - TEST_DB_HOST=db-test
    depends_on:
      - db-test
    command: npm run test:vendor-a

  vendor-b-test:
    build:
      context: .
      dockerfile: dockerfile.vendor-b.test
    environment:
      - NODE_ENV=test
      - TEST_DB_HOST=db-test
    depends_on:
      - vendor-a-test
      - db-test
    command: npm run test:vendor-b

  integration-test:
    build:
      context: .
      dockerfile: dockerfile.test
    environment:
      - NODE_ENV=test
      - TEST_DB_HOST=db-test
    depends_on:
      - vendor-a-test
      - vendor-b-test
      - db-test
    command: npm run test:integration

  db-test:
    image: postgres:14
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_USER: test
      POSTGRES_DB: app_test
    tmpfs: /var/lib/postgresql/data

デプロイ戦略

モジュラーモノリスの主な強みの一つが柔軟なデプロイ戦略です。状況に応じて最適な方法を選択できます:

1. 単一アプリケーションとしてのデプロイ

開発初期や小規模なシステムでは、全モジュールを単一のアプリケーションとしてデプロイ:

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    depends_on:
      - db
  
  db:
    image: postgres:14
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_USER: user
      POSTGRES_DB: app

volumes:
  postgres_data:

2. モジュールごとの個別デプロイ

負荷やスケーリング要件に応じて、モジュールごとに個別のコンテナとしてデプロイできます:

# docker-compose.prod.yml
version: '3.8'

services:
  vendor-a:
    build:
      context: .
      dockerfile: dockerfile.vendor-a
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=production
    depends_on:
      - db
    deploy:
      replicas: 3  # 負荷に応じてスケール

  vendor-b:
    build:
      context: .
      dockerfile: dockerfile.vendor-b
    ports:
      - "3002:3000"
    environment:
      - NODE_ENV=production
      - VENDOR_A_API_URL=http://vendor-a:3000
    depends_on:
      - vendor-a
      - db
  
  db:
    image: postgres:14
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_USER: user
      POSTGRES_DB: app

volumes:
  postgres_data:

それぞれのモジュール用のエントリーポイントとDockerfileを用意:

// src/vendor-a/main.ts
import { NestFactory } from '@nestjs/core';
import { VendorAModule } from './vendor-a.module';

async function bootstrap() {
  const app = await NestFactory.create(VendorAModule);
  await app.listen(3000);
}
bootstrap();
# dockerfile.vendor-a
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

CMD ["node", "dist/vendor-a/main.js"]

モジュール単位での切り替えとスケーリングのメリット

モジュール単位でコンテナを分離することには、以下のような大きなメリットがあります:

  1. 選択的スケーリング:

    • 負荷の高いモジュールだけをスケールアウト可能
    • リソース利用の最適化とコスト削減
  2. 独立したデプロイサイクル:

    • 各ベンダーが独自のリリーススケジュールで開発・デプロイ可能
    • 一部モジュールの変更が他に影響しない
  3. 障害の隔離:

    • 特定モジュールの問題が全体に波及しにくい
    • 影響範囲を限定したロールバックが可能
  4. テクノロジースタックの柔軟性:

    • モジュールごとに異なるNode.jsバージョンの利用も可能
    • 段階的な技術更新が容易

まとめ

NestJSを使ったモジュラーモノリスは、複数ベンダーによる協業開発において優れたアーキテクチャ選択肢となります。

この記事で説明したように:

  1. 明確なモジュール境界: 各ベンダーの担当範囲を明確に分離
  2. インターフェースによる制御: 必要な機能だけを公開し、実装詳細を隠蔽
  3. 柔軟な通信方法: API呼び出しやイベント駆動型アーキテクチャの選択
  4. 柔軟なデプロイ: 単一アプリケーションから個別コンテナまで対応可能
  5. 効率的な開発とテスト: Docker Composeを用いた結合テストと統合環境
  6. モジュール単位の切り替え: 負荷や要件に応じて最適なデプロイ構成を選択可能

モジュラーモノリスの採用により、企業間の協業プロジェクトでの開発効率と保守性を大幅に向上させることができます。マイクロサービスの複雑さを避けつつ、モノリスの柔軟性の低さも克服する「中間的なアプローチ」として、多くのプロジェクトで検討する価値があるでしょう。

特に複数ベンダーが参加するプロジェクトでは、責任範囲の明確化とインターフェースを通じた協業により、それぞれの専門分野に集中した効率的な開発が可能になります。システムの成長に応じて、一部のモジュールだけをマイクロサービス化するという段階的な進化も容易に実現できます。

Discussion