0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LaravelでModular Monolithを実装した話 ― マイクロサービスにせず200テーブルを管理する設計

0
Last updated at Posted at 2026-03-12

はじめに

株式会社トラストバンクで地域通貨プラットフォーム chiica(チーカ)の開発責任者をしております湊(みなと)(@karura618)です。
今回は、 前回の記事 の続きとしてLaravelでのアーキテクチャ設計時に Modular Monolith を実装した話をさせていただきます。

マイクロサービスアーキテクチャが注目される一方で、
その複雑さに悩むチームも増えています。

  • サービス間通信のオーバーヘッド
  • 分散トランザクションの複雑さ
  • デプロイ・運用コストの増加
  • チームの技術レベルとのギャップ

しかし、単純なモノリスに戻ると
今度は 以下の問題 が発生します。

  • 200テーブルが1つのディレクトリに混在
  • ドメイン境界が曖昧
  • コード変更の影響範囲が不明確

そこで今回採用したのが

Modular Monolith(モジュラーモノリス)

です。

この記事では、

  • Modular Monolithとは何か
  • Laravelでどう実装したか
  • 200テーブル規模での運用結果

を実際のコード例とともに解説します。

本記事は以下の記事の続編です。
LaravelでRepository Patternを採用しなかった理由

本記事は特定のサービスやプロダクトの内容を含まず、一般的なアーキテクチャ設計の観点で書かれています。


結論(先にまとめ)

本プロジェクトでは以下のアーキテクチャを採用しました。

  • 単一のLaravelアプリケーション(モノリス)
  • スキーマ分離によるBounded Context実現
  • 18DB接続の明確な管理
  • Read/Write分離(CQRS的アプローチ)
  • マイクロサービスへの移行可能性を保持

結果として

  • マイクロサービスの複雑さを回避
  • コンテキスト境界を明確化
  • デプロイ・運用コストの削減
  • 200テーブルでも破綻しない設計

を実現することができました。


この記事が役に立つ人

  • マイクロサービスの複雑さに悩んでいる
  • モノリスの限界を感じている
  • 大規模Laravelプロジェクトの設計を知りたい
  • Bounded Contextの実装方法を知りたい
  • Modular Monolithについて知りたい

Modular Monolithとは何か

定義

Modular Monolith(モジュラーモノリス) とは、

単一のアプリケーション(モノリス)だが
内部的に明確なモジュール境界を持つアーキテクチャ

です。

3つのアーキテクチャの比較

従来型モノリス

app/
└── Models/
    ├── User.php
    ├── Member.php
    ├── Post.php
    ├── Comment.php
    ├── Product.php
    ├── Order.php
    ├── Payment.php
    ├── Point.php
    ├── Program.php
    └── ... (200個全部フラット)

問題点:

  • すべてが1つのディレクトリに混在
  • ドメイン境界が不明確
  • コード変更の影響範囲が分からない
  • 200テーブル規模では破綻する

マイクロサービス

member-service/
  └── Models/
      ├── Member.php
      └── ProvisionalMember.php

content-service/
  └── Models/
      ├── Post.php
      └── Comment.php

payment-service/
  └── Models/
      ├── Payment.php
      └── Order.php

メリット:

  • ✅ コンテキスト境界が明確
  • ✅ 独立したデプロイが可能
  • ✅ 技術スタックの自由度

デメリット:

  • ❌ サービス間通信のオーバーヘッド
  • ❌ 分散トランザクションの複雑さ
  • ❌ デプロイ・運用コストの増加
  • ❌ チームの学習コスト

Modular Monolith(私たちの選択)

app/
└── Models/
    ├── MemberDB/              # 会員コンテキスト
    │   ├── Member.php
    │   └── ProvisionalMember.php
    │
    └── ServiceDB/            # サービスコンテキスト
        ├── LoginHistory.php
        ├── Program.php
        └── Point.php

メリット:

  • ✅ コンテキスト境界が明確(スキーマ分離)
  • ✅ 単一のデプロイ(シンプル)
  • ✅ サービス間通信なし(高速)
  • ✅ トランザクション管理が簡単
  • ✅ マイクロサービスへの移行も可能

デメリット:

  • ⚠️ 完全な独立デプロイは不可
  • ⚠️ 技術スタックは統一

なぜModular Monolithを選んだか

プロジェクトの特性

本記事のアーキテクチャは
次のような規模のLaravelプロジェクトで採用しています。

  • テーブル数: 200+
  • DB接続数: 18
  • DBスキーマ: 2つの主要スキーマ + その他
  • 外部API: 決済 / ポイント / SMS
  • ドメイン: 金融・決済系
  • チーム開発: 複数エンジニア

マイクロサービスを選ばなかった理由

1. 分散トランザクションの複雑さ

金融・決済系では、

会員情報の更新 + ポイント付与

のようなアトミックな操作が頻繁に発生します。

マイクロサービスでは、

// ❌ マイクロサービス: 分散トランザクション
try {
    // Service 1: 会員サービス
    $memberService->updateMember($memberId, $data);

    // Service 2: ポイントサービス
    $pointService->grantPoints($memberId, $points);

    // → どちらかが失敗したら?
    // → Sagaパターン? 2フェーズコミット?
} catch (Exception $e) {
    // ロールバックの実装が複雑...
}

Modular Monolithでは、

// ✅ Modular Monolith: 単一トランザクション
DB::beginTransaction();
try {
    // 会員情報更新(MemberDBスキーマ)
    $member = Member::on('memberdb_w')->find($memberId);
    $member->update($data);

    // ポイント付与(ServiceDBスキーマ)
    MemberPoint::on('servicedb_w')->create([
        'member_id' => $memberId,
        'points' => $points,
    ]);

    DB::commit();
} catch (Exception $e) {
    DB::rollBack();
    throw $e;
}

判断: トランザクション管理のシンプルさを優先

2. サービス間通信のオーバーヘッド

マイクロサービスでは、

会員情報取得 → HTTP/gRPC → レイテンシ増加
ポイント取得 → HTTP/gRPC → レイテンシ増加
プログラム取得 → HTTP/gRPC → レイテンシ増加

Modular Monolithでは、

// 単一プロセス内で完結
$member = Member::find($memberId);
$points = $member->points;  // Relationship
$program = $points->program;  // Eager Loading

判断: レスポンスタイムを優先

3. デプロイ・運用コストの増加

マイクロサービスでは、

会員サービス: デプロイ、監視、ログ集約
ポイントサービス: デプロイ、監視、ログ集約
決済サービス: デプロイ、監視、ログ集約
→ 運用コスト 3倍

Modular Monolithでは、

単一アプリケーション: デプロイ、監視、ログ集約
→ 運用コスト 1倍

判断: 運用コストを最小化

従来型モノリスを選ばなかった理由

200テーブルを1つのディレクトリに配置すると、

app/Models/
├── User.php
├── Member.php
├── ... (200個フラット)

問題:

  • どのModelがどのドメインに属するか不明確
  • コード変更の影響範囲が分からない
  • チーム開発でコンフリクトが頻発

判断: ドメイン境界を明確にする必要がある


LaravelでBounded Contextを実現する

Bounded Contextとは

Bounded Context(境界づけられたコンテキスト) は、
DDDの最も重要な概念の1つです。

簡単に言うと、

「意味の境界」

です。

例えば、「会員(Member)」という言葉は、

会員管理コンテキスト:
  → 電話番号、メールアドレス、認証情報

サービス管理コンテキスト:
  → ポイント、プログラム参加履歴、ランク

のように、コンテキストによって意味が異なります。

スキーマ分離によるBounded Context実現

本プロジェクトでは、

データベーススキーマを分離することで、Bounded Contextを実現しています。

主要な2つのコンテキスト

MemberDB(会員管理コンテキスト)
  ↓
  会員情報、認証、個人情報

ServiceDB(サービス管理コンテキスト)
  ↓
  ポイント、プログラム、ログ、通知

ディレクトリ構成

app/
├── Models/
│   ├── MemberDB/              # 会員管理コンテキスト
│   │   ├── Member.php
│   │   ├── ProvisionalMember.php
│   │   ├── MemberTel.php
│   │   └── MemberNickname.php
│   │
│   └── ServiceDB/            # サービス管理コンテキスト
│       ├── MemberLoginLog.php
│       ├── MemberPoint.php
│       ├── MemberPointLog.php
│       ├── Program.php
│       ├── ProgramDistribution.php
│       ├── City.php
│       └── CityGroup.php
│
├── UseCases/
│   ├── LoginUseCase.php              # 会員コンテキスト使用
│   ├── RegisterMainMemberUseCase.php  # 会員コンテキスト使用
│   └── PointListUseCase.php          # 両コンテキスト使用
│
└── Services/
    ├── Auth/
    │   ├── TokenService.php          # 会員コンテキスト使用
    │   └── MemberAuthService.php     # 会員コンテキスト使用
    │
    └── Point/
        └── PointService.php  # サービスコンテキスト使用

Models配置の重要性

従来型モノリス(悪い例):

// ❌ BAD: すべて同じディレクトリ
app/Models/
├── Member.php
├── ProvisionalMember.php
├── MemberLoginLog.php
├── MemberPoint.php
├── Program.php
└── ... (200個フラット)

問題:

  • どのModelがどのスキーマに属するか分からない
  • コンテキスト境界が不明確

Modular Monolith(良い例):

// ✅ GOOD: スキーマ別にディレクトリ分離
app/Models/
├── MemberDB/
   ├── Member.php
   └── ProvisionalMember.php

└── ServiceDB/
    ├── MemberLoginLog.php
    ├── MemberPoint.php
    └── Program.php

メリット:

  • スキーマが一目で分かる
  • コンテキスト境界が明確
  • チーム開発でコンフリクトしにくい

18DB接続の管理

接続構成

本プロジェクトでは、18のDB接続を管理しています。

config/database.php:

// 会員管理コンテキスト(MemberDBスキーマ)
'memberdb_r' => [...],   // Read専用
'memberdb_w' => [...],   // Write専用

// サービス管理コンテキスト(ServiceDBスキーマ)
'servicedb_r' => [...],   // Read専用
'servicedb_w' => [...],   // Write専用

// その他14接続
...

Read/Write分離

各コンテキストでRead/Write分離を実装しています。

// ✅ Read操作: Read専用接続
$member = Member::on('memberdb_r')
    ->where('tel', $tel)
    ->first();

// ✅ Write操作: Write専用接続
$member->setConnection('memberdb_w');
$member->save();

メリット:

  • レプリケーション遅延の考慮
  • Read負荷分散
  • Write性能の確保

Model実装例

MemberDB(会員管理コンテキスト)のModel

namespace App\Models\MemberDB;

use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    // ============================================================
    // コンテキスト: 会員管理(MemberDB)
    // ============================================================

    protected $table = 'members';

    // デフォルトはRead専用接続
    protected $connection = 'memberdb_r';

    public const MEMBER_TYPE_APP = 1;
    public const MEMBER_TYPE_CARD = 2;

    protected $fillable = [
        'tel',
        'member_code',
        'password',
        'member_type',
        'stop_flag',
        'auth_failed_count',
        'account_unlock_date',
    ];

    // ============================================================
    // ビジネスルール(会員コンテキストのドメインロジック)
    // ============================================================

    /**
     * ログイン可能かチェック
     */
    public function ensureCanLogin(): void
    {
        if ($this->isStopped()) {
            throw new AccountStoppedException;
        }

        if ($this->isLocked()) {
            throw new AccountLockedException(
                $this->account_unlock_date
            );
        }
    }

    /**
     * 認証失敗を記録
     */
    public function recordAuthenticationFailure(int $lockMinutes): void
    {
        // 既にロック中だがロック期限が過ぎている場合はリセット
        if ($this->auth_failed_count >= self::LIMIT_AUTH_FAILED_COUNT
            && $this->account_unlock_date !== null
            && $this->account_unlock_date <= now()) {
            $this->auth_failed_count = 1;
            $this->account_unlock_date = null;
        } else {
            $this->auth_failed_count++;

            // リミットに達したらアカウントロック
            if ($this->auth_failed_count >= self::LIMIT_AUTH_FAILED_COUNT) {
                $this->account_unlock_date = now()->addMinutes($lockMinutes);
            }
        }
    }
}

ServiceDB(サービス管理コンテキスト)のModel

namespace App\Models\ServiceDB;

use Illuminate\Database\Eloquent\Model;

class MemberPoint extends Model
{
    // ============================================================
    // コンテキスト: コンテンツ管理(ServiceDB)
    // ============================================================

    protected $table = 'member_points';

    // デフォルトはRead専用接続
    protected $connection = 'servicedb_r';

    protected $fillable = [
        'member_id',
        'city_id',
        'point',
        'program_id',
    ];

    // ============================================================
    // Relationship(サービスコンテキスト内)
    // ============================================================

    public function program()
    {
        return $this->belongsTo(Program::class, 'program_id');
    }

    public function city()
    {
        return $this->belongsTo(City::class, 'city_id');
    }
}

重要なポイント:

  • namespaceでコンテキストを明確化
  • $connectionでデフォルト接続を指定
  • ビジネスルールは各コンテキストに集約

UseCaseでのコンテキスト利用

単一コンテキストの例: LoginUseCase

namespace App\UseCases;

use App\DTOs\LoginRequest;
use App\DTOs\LoginResponse;
use App\Models\MemberDB\Member;
use App\Models\ServiceDB\MemberLoginLog;
use App\Services\Auth\TokenService;
use Illuminate\Support\Facades\DB;

class LoginUseCase
{
    public function __construct(
        private TokenService $tokenService
    ) {}

    public function execute(LoginRequest $request): LoginResponse
    {
        // ============================================================
        // 会員管理コンテキスト(MemberDB)の操作
        // ============================================================

        // Read操作
        $member = Member::on('memberdb_r')
            ->where('tel', $request->tel)
            ->where('member_type', Member::MEMBER_TYPE_APP)
            ->first();

        if ($member === null) {
            throw new MemberNotFoundException;
        }

        // ビジネスルール実行
        $member->ensureCanLogin();

        if (!$member->authenticate($request->password)) {
            $this->recordAuthenticationFailure($member);
            throw new InvalidPasswordException;
        }

        // ============================================================
        // トランザクション開始(複数コンテキストにまたがる)
        // ============================================================

        DB::connection('memberdb_w')->beginTransaction();
        DB::connection('servicedb_w')->beginTransaction();

        try {
            // Write操作(会員コンテキスト)
            $member->recordAuthenticationSuccess();
            $member->setConnection('memberdb_w');
            $member->save();

            // Write操作(サービスコンテキスト)
            MemberLoginLog::on('servicedb_w')->create([
                'member_id' => $member->id,
                'logined' => now(),
            ]);

            DB::connection('memberdb_w')->commit();
            DB::connection('servicedb_w')->commit();
        } catch (Exception $e) {
            DB::connection('memberdb_w')->rollBack();
            DB::connection('servicedb_w')->rollBack();
            throw $e;
        }

        // トークン生成
        $token = $this->tokenService->generateLoginToken($member);

        return new LoginResponse(
            loginToken: $token->token(),
            memberId: $token->memberId(),
            expiredAt: $token->expiredAt()->format('Y/m/d H:i:s')
        );
    }

    private function recordAuthenticationFailure(Member $member): void
    {
        DB::connection('memberdb_w')->beginTransaction();

        try {
            $member->recordAuthenticationFailure(30);
            $member->setConnection('memberdb_w');
            $member->save();

            DB::connection('memberdb_w')->commit();
        } catch (Exception $e) {
            DB::connection('memberdb_w')->rollBack();
            throw $e;
        }
    }
}

重要なポイント:

  • UseCaseがコンテキスト間の境界を超える唯一の場所
  • トランザクションは明示的に管理
  • Read/Write分離を徹底

コンテキスト間の通信

原則: UseCase経由でのみ通信

Modular Monolithでは、

コンテキスト間の直接アクセスを禁止

しています。

❌ BAD: Model間で直接Relationship

// ❌ BAD: コンテキスト境界を超えるRelationship
namespace App\Models\ServiceDB;

class MemberPoint extends Model
{
    // ❌ ServiceDB → MemberDB への直接参照
    public function member()
    {
        return $this->belongsTo(
            \App\Models\MemberDB\Member::class,
            'member_id'
        );
    }
}

問題:

  • コンテキスト間の結合度が高まる
  • マイクロサービス化が困難になる

✅ GOOD: UseCase経由で結合

// ✅ GOOD: UseCase層で結合
namespace App\UseCases;

class PointListUseCase
{
    public function execute(PointListRequest $request): PointListResponse
    {
        // 会員コンテキストから会員情報取得
        $member = Member::on('memberdb_r')
            ->find($request->memberId);

        // サービスコンテキストからポイント情報取得
        $points = MemberPoint::on('servicedb_r')
            ->where('member_id', $request->memberId)
            ->get();

        // UseCase層で結合
        return new PointListResponse(
            member: $member,
            points: $points
        );
    }
}

メリット:

  • コンテキスト境界が明確
  • 将来的なマイクロサービス化が容易

トランザクション境界の明確化

原則: UseCaseがトランザクション境界

class PaymentUseCase
{
    public function execute(PaymentRequest $request): PaymentResponse
    {
        // ============================================================
        // トランザクション開始(複数コンテキスト)
        // ============================================================

        DB::connection('memberdb_w')->beginTransaction();
        DB::connection('servicedb_w')->beginTransaction();

        try {
            // 会員情報更新(MemberDBコンテキスト)
            $member = Member::on('memberdb_w')
                ->find($request->memberId);
            $member->updateLastPaymentDate();
            $member->save();

            // ポイント付与(ServiceDBコンテキスト)
            MemberPoint::on('servicedb_w')->create([
                'member_id' => $member->id,
                'point' => $request->points,
            ]);

            // ログ記録(ServiceDBコンテキスト)
            PurchaseLog::on('servicedb_w')->create([
                'member_id' => $member->id,
                'amount' => $request->amount,
            ]);

            DB::connection('memberdb_w')->commit();
            DB::connection('servicedb_w')->commit();
        } catch (Exception $e) {
            DB::connection('memberdb_w')->rollBack();
            DB::connection('servicedb_w')->rollBack();
            throw $e;
        }

        return new PaymentResponse(...);
    }
}

重要なポイント:

  • UseCaseがトランザクション境界を定義
  • 複数コンテキストにまたがる操作も単一トランザクションで管理
  • マイクロサービスではSagaパターンが必要になる部分

Mermaid図: アーキテクチャ全体像


メリット・デメリット

メリット

1. マイクロサービスの複雑さを回避

  • ✅ サービス間通信なし(プロセス内メモリアクセス)
  • ✅ 分散トランザクション不要(単一DB複数スキーマ)
  • ✅ デプロイが簡単(単一アプリケーション)
  • ✅ 運用コストが低い(監視、ログ集約が1つ)

2. 従来型モノリスより明確な境界

  • ✅ Bounded Contextが明確(スキーマ分離)
  • ✅ ドメイン境界が分かりやすい
  • ✅ チーム分担が明確(コンテキスト単位)

3. Eloquentの強力な機能を活用

  • ✅ Relationship、Scope、Accessor、Mutator
  • ✅ Eager Loading(N+1問題対策)
  • ✅ Query Builder

4. マイクロサービスへの移行可能性

  • ✅ コンテキスト境界が明確なので分割しやすい
  • ✅ UseCase経由でのみ通信(疎結合)

5. 開発速度の向上

  • ✅ サービス間通信の実装不要
  • ✅ デプロイが簡単
  • ✅ ローカル開発環境が軽量

デメリット(認識した上で受け入れる)

1. 完全な独立デプロイは不可

  • ⚠️ 単一アプリケーションなので、部分的なデプロイは不可
  • ⚠️ 全体をデプロイする必要がある

判断: デプロイ頻度が低く、ダウンタイムも許容範囲内

2. 技術スタックは統一

  • ⚠️ すべてLaravel + PHP
  • ⚠️ コンテキストごとに技術を変えることは不可

判断: Laravelで十分な機能を実現できる

3. スケーラビリティの制約

  • ⚠️ 水平スケーリングは可能だが、コンテキスト単位でのスケーリングは不可
  • ⚠️ すべてのコンテキストが同時にスケールする

判断: 現在のトラフィックでは問題ない

4. チーム分割の制約

  • ⚠️ 完全な独立開発は困難
  • ⚠️ 同じリポジトリ、同じデプロイサイクル

判断: チームサイズが大きくないので問題ない


マイクロサービスへの移行可能性

現在のアーキテクチャ: Modular Monolith

Laravel Application
├─ MemberDBコンテキスト
│   └─ Models, Services, UseCases
└─ ServiceDBコンテキスト
    └─ Models, Services, UseCases

将来的なマイクロサービス化

もしトラフィックが増大し、
マイクロサービス化が必要になった場合、

コンテキスト境界が明確なので、比較的容易に分割できます。

Member Service (Laravel)
├─ MemberDBコンテキスト
└─ Models, Services, UseCases

Content Service (Laravel)
├─ ServiceDBコンテキスト
└─ Models, Services, UseCases

移行手順:

  1. API境界の定義

    • UseCase層をREST APIに変換
    • コンテキスト間の通信をHTTP/gRPCに変更
  2. トランザクション境界の再設計

    • 単一トランザクション → Sagaパターン
    • 補償トランザクションの実装
  3. データベース分離

    • スキーマ分離 → 物理的なDB分離
    • レプリケーション設定
  4. デプロイ環境の分離

    • 単一アプリケーション → 複数サービス
    • サービスディスカバリーの導入

重要なポイント:

  • コンテキスト境界が明確なので、分割しやすい
  • UseCase経由でしか通信していないので、API化が容易

実際に運用してみた結果

この構成で数年運用していますが、以下のメリットを実感しています。

開発面

  • コンテキスト境界が明確で開発しやすい

    • どのコンテキストに属するか一目で分かる
    • 新しいメンバーでも迷わない
  • チーム分担が明確

    • 会員管理チーム → MemberDBコンテキスト
    • コンテンツチーム → ServiceDBコンテキスト
    • コンフリクトが減る
  • Eloquentの便利機能を活用できる

    • Relationship、Scope、Accessor、Mutatorを駆使
    • N+1問題もEager Loadingで解決

運用面

  • デプロイが簡単

    • 単一アプリケーション
    • デプロイ時間: 約5分
    • ダウンタイム: ほぼゼロ(Blue-Green Deployment)
  • 監視・ログが集約されている

    • 1つのアプリケーションを監視すればOK
    • ログ集約が簡単
  • トラブルシューティングが容易

    • ログを1箇所で確認
    • トランザクション境界が明確

パフォーマンス面

  • レスポンスタイムが速い

    • サービス間通信なし
    • プロセス内メモリアクセス
    • 平均レスポンスタイム: 50ms以下
  • トランザクション管理が確実

    • 単一DBトランザクション
    • ロールバックが確実

この設計が向いているプロジェクト

このアーキテクチャは、すべてのプロジェクトに最適というわけではありません。

特に次のようなプロジェクトでは有効だと考えています。

Modular Monolithが向いているケース

  • 中規模〜大規模のモノリス(100〜500テーブル)
  • ドメイン境界が明確だが、マイクロサービスほど分割する必要はない
  • トランザクション整合性が重要(金融・決済系)
  • チームサイズが中規模(5〜20人程度)
  • デプロイ・運用コストを抑えたい
  • 将来的なマイクロサービス化の可能性を残したい

マイクロサービスが向いているケース

  • 超大規模プロジェクト(1000テーブル以上)
  • 完全に独立したチーム(30人以上)
  • コンテキストごとに異なる技術スタック
  • コンテキストごとに異なるスケーリング要件
  • 結果整合性で問題ない

従来型モノリスが向いているケース

  • 小規模プロジェクト(50テーブル以下)
  • ドメイン境界が不明確
  • プロトタイプ・MVP

まとめ

Modular Monolithの本質

本プロジェクトでは、

マイクロサービスの「コンテキスト境界の明確さ」と、モノリスの「シンプルさ」の両方を実現する

ために、Modular Monolithを採用しました。

重要な判断:

  1. スキーマ分離でBounded Contextを実現

    • Models配置でコンテキストを明確化
    • MemberDB / ServiceDB の2つの主要コンテキスト
  2. UseCase層でトランザクション境界を定義

    • コンテキスト間通信はUseCase経由のみ
    • 単一トランザクションで整合性を保証
  3. Read/Write分離で性能を確保

    • 18DB接続の明確な管理
    • レプリケーション遅延を考慮
  4. マイクロサービス化の可能性を保持

    • コンテキスト境界が明確
    • UseCase経由でのみ通信

200テーブルでも破綻しない理由

従来型モノリス:
  → 200テーブルが1つのディレクトリに混在
  → ドメイン境界が不明確
  → 破綻

Modular Monolith:
  → スキーマ分離でBounded Context実現
  → ドメイン境界が明確
  → 200テーブルでも管理可能

アーキテクチャは「正解」ではなく「選択」

マイクロサービス、Modular Monolith、従来型モノリス、

どれが正しいというより、

「プロジェクトの特性・チームの状況・技術的要件」に合うかどうか

が最も重要だと考えています。

私たちのプロジェクトでは、

  • 金融・決済系(トランザクション整合性が重要)
  • 中規模チーム
  • 運用コストを抑えたい

という特性から、Modular Monolithが最適でした。


参考資料


以上、LaravelでModular Monolithを実装した話でした。


📚 前編記事

本記事は以下の記事の続編です。
まだ読んでいない方は、ぜひ前編もご覧ください。

👉 LaravelでRepository Patternを採用しなかった理由 - 200テーブル規模の実務で辿り着いたシンプルなアーキテクチャ

前編では、

  • なぜRepository Patternを採用しなかったか
  • Active Recordパターンの活用方法
  • UseCase / Service / Modelの責務分離
  • テスト戦略

について解説しています。


エンジニア募集

弊社では絶賛エンジニア募集中です!
気になった方、是非お気軽に Wantedly からご連絡ください!


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?