mybook

モノリシックアーキテクチャ — 一枚岩の価値

「モノリスって時代遅れじゃないの?」

チームメンバーの田中ユウが言った。週次アーキテクチャ勉強会の最中だ。

「Twitterがマイクロサービスに移行した話、読みましたよ。あれが正解じゃないですか?」

カオリは内心、少し前の自分も同じことを思っていた。しかし今は違う答えを持っている。

「Twitterが移行したのは、ユーザー数が数億人に達してからの話。今の私たちは10万ユーザー目標の6人チーム。文脈が全く違う」

INFO

モノリシックアーキテクチャとは、アプリケーションの全機能が単一のデプロイ単位として動作するシステムです。「古い」のではなく、「適切な文脈がある」設計パターンです。

モノリスの構造

モノリスはシンプルだ。一つのコードベース、一つのデータベース、一つのデプロイ。Railsで言えば、これが最も自然な形だ。

Loading diagram...

コードの中では、Railsの標準的な構成が力を発揮する。

# app/models/order.rb
class Order < ApplicationRecord
  belongs_to :user
  belongs_to :product
  has_many :order_items
  
  validates :status, inclusion: { in: %w[pending paid shipped delivered] }
  
  # ビジネスロジックがモデルに集約される
  def total_price
    order_items.sum { |item| item.quantity * item.unit_price }
  end
  
  def can_cancel?
    status.in?(%w[pending paid])
  end
end
 
# app/services/order_service.rb
class OrderService
  def create_order(user:, cart:)
    ActiveRecord::Base.transaction do
      order = Order.create!(user: user, status: 'pending')
      
      cart.items.each do |cart_item|
        order.order_items.create!(
          product: cart_item.product,
          quantity: cart_item.quantity,
          unit_price: cart_item.product.price
        )
        
        # 在庫更新も同一トランザクション内
        cart_item.product.decrement!(:stock_count, cart_item.quantity)
      end
      
      order
    end
  end
end

モノリスの5つの強み

1. 開発速度が高い

単一のコードベースだから、機能をまたいだ変更が一箇所で完結する。APIを追加したら、フロントエンドもバックエンドも同じPRに入る。データベースのスキーマ変更も一つのマイグレーションで済む。

# 新機能を追加するとき、全てのレイヤーを一度に変更できる
# マイグレーション
rails generate migration AddCouponToOrders coupon_code:string discount_amount:decimal
 
# モデル
class Order < ApplicationRecord
  attribute :coupon_code, :string
  attribute :discount_amount, :decimal, default: 0
  
  def final_price
    [total_price - discount_amount, 0].max
  end
end
 
# コントローラ
class OrdersController < ApplicationController
  def create
    result = OrderService.new.create_order(
      user: current_user,
      cart: current_cart,
      coupon_code: params[:coupon_code]
    )
    # ...
  end
end

マイクロサービスだと、注文サービス・クーポンサービス・在庫サービスを別々に変更してAPIを合わせる必要がある。6つのPRと3つのデプロイが必要になることも。

2. トランザクション管理が簡単

モノリスの最大の恩恵の一つが、データベーストランザクションだ。

# 注文・在庫・決済を全て同一トランザクションで処理できる
ActiveRecord::Base.transaction do
  order = Order.create!(...)
  inventory.decrement_stock!(...)
  payment = PaymentRecord.create!(...)
  
  # どこかで例外が起きれば全部ロールバック
  # マイクロサービスでこれをやろうとすると
  # 分散トランザクション(Saga パターン等)が必要になる
end

マイクロサービスでこの整合性を保つには、Sagaパターンや補償トランザクションという複雑な仕組みが必要だ。

3. デバッグとトレーシングが容易

# ログにリクエストIDを付与するだけで、一連の処理を追える
class ApplicationController < ActionController::Base
  before_action :set_request_id
  
  private
  
  def set_request_id
    RequestStore.store[:request_id] = request.uuid
    Rails.logger.info "[#{request.uuid}] #{request.method} #{request.path}"
  end
end

モノリスでは全てのログが一箇所に集まる。マイクロサービスでは分散トレーシング(Jaeger, Zipkin等)が必要になり、インフラの複雑さが増す。

4. テストが書きやすい

# RSpecで統合テストが自然に書ける
RSpec.describe 'Order creation flow' do
  it 'creates order, decrements inventory, and sends confirmation email' do
    product = create(:product, stock_count: 10)
    user = create(:user)
    
    # 全部が同一プロセスで動くので、直接確認できる
    expect {
      post '/api/orders', params: { product_id: product.id, quantity: 2 }
    }.to change { Order.count }.by(1)
      .and change { product.reload.stock_count }.by(-2)
      .and have_enqueued_mail(OrderMailer, :confirmation)
  end
end

5. 運用がシンプル

# AWSでのモノリス構成
# インフラはシンプルでコストも低い
AWSデプロイ構成:
  - EC2 or ECS (単一サービス)
  - RDS (PostgreSQL)
  - ElastiCache (Redis)
  - ALB (ロードバランサー)
  
必要なアラート:
  - CPU使用率
  - メモリ使用率
  - エラーレート
  - レスポンスタイム

AWSでのモノリス構成

カオリはAWS構成図を描いた。

Loading diagram...
# ECSタスク定義(簡略版)
taskDefinition:
  family: mybook-ec-web
  cpu: "512"
  memory: "1024"
  containerDefinitions:
    - name: rails
      image: "123456789.dkr.ecr.ap-northeast-1.amazonaws.com/mybook-ec:latest"
      portMappings:
        - containerPort: 3000
      environment:
        - name: DATABASE_URL
          valueFrom: "arn:aws:secretsmanager:..."
        - name: REDIS_URL
          value: "redis://mybook-redis.xxx.cache.amazonaws.com:6379"
      healthCheck:
        command: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]

モノリスの限界

もちろん、モノリスにも限界はある。カオリはそれも正直に認識していた。

スケーリングの粗さ

「注文処理の負荷が高い時に、商品閲覧も遅くなってしまう」

モノリスは全機能が同じプロセスで動くため、特定の機能だけスケールさせることができない。全体を水平スケールするしかない。

Loading diagram...

デプロイの影響範囲

一箇所のバグが全体に影響する。商品カタログの変更で決済機能がダウンしたら、ビジネスへの影響は甚大だ。

チームの並行開発の摩擦

チームが大きくなると、同じコードベースへの変更が衝突しやすくなる。マージコンフリクトが増え、大きなPRがデプロイを詰まらせる。

「モジュラーモノリス」という選択肢

カオリが提案したのは、純粋なモノリスでも分散システムでもない、中間のアプローチだった。

モジュラーモノリス: 単一デプロイ単位を保ちつつ、コードを明確なモジュールに分割する。

# Rails Engine を使ったモジュール分割
# engines/catalog/app/models/catalog/product.rb
module Catalog
  class Product < ApplicationRecord
    self.table_name = 'catalog_products'
    
    validates :name, presence: true
    validates :price, numericality: { greater_than: 0 }
  end
end
 
# engines/ordering/app/models/ordering/order.rb
module Ordering
  class Order < ApplicationRecord
    self.table_name = 'ordering_orders'
    
    # CatalogモジュールへはAPIを通じてアクセス(直接モデルを触らない)
    def product
      Catalog::ProductFinder.find(product_id)
    end
  end
end

これにより、将来的にマイクロサービスへ分割する際の境界が明確になる。

INFO

Shopify, GitHub, Basecamp はモジュラーモノリスで大規模サービスを運営しています。モノリス = 小規模というわけではありません。

実践:いつモノリスを選ぶか

カオリはチームに判断基準を示した。

モノリスが適切な状況:
✅ チームが10人未満
✅ ドメインがまだ探索フェーズ
✅ 運用の専門家がいない(DevOpsが少ない)
✅ 初年度ユーザー数が数十万以下
✅ 開発速度が最優先

マイクロサービス移行を検討する状況:
⚠️  特定機能だけが極端に高負荷
⚠️  チームが独立してデプロイしたい
⚠️  ドメインの境界が明確になってきた
⚠️  ダウンタイムが許容されない機能がある

「今の私たちは全部✅だ」とカオリは言った。「だからまずはモノリスから始める。ただしモジュラーモノリスとして、将来の分割を意識した境界で設計する」

チームは納得した。これがカオリの最初のアーキテクチャ判断だった。


次章では、モノリス内の設計を整理する「レイヤードアーキテクチャ」を学ぶ。コードを水平な層に分けることで、保守性を高める方法を見ていこう。