モノリシックアーキテクチャ — 一枚岩の価値
「モノリスって時代遅れじゃないの?」
チームメンバーの田中ユウが言った。週次アーキテクチャ勉強会の最中だ。
「Twitterがマイクロサービスに移行した話、読みましたよ。あれが正解じゃないですか?」
カオリは内心、少し前の自分も同じことを思っていた。しかし今は違う答えを持っている。
「Twitterが移行したのは、ユーザー数が数億人に達してからの話。今の私たちは10万ユーザー目標の6人チーム。文脈が全く違う」
INFO
モノリシックアーキテクチャとは、アプリケーションの全機能が単一のデプロイ単位として動作するシステムです。「古い」のではなく、「適切な文脈がある」設計パターンです。
モノリスの構造
モノリスはシンプルだ。一つのコードベース、一つのデータベース、一つのデプロイ。Railsで言えば、これが最も自然な形だ。
コードの中では、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
end5. 運用がシンプル
# AWSでのモノリス構成
# インフラはシンプルでコストも低い
AWSデプロイ構成:
- EC2 or ECS (単一サービス)
- RDS (PostgreSQL)
- ElastiCache (Redis)
- ALB (ロードバランサー)
必要なアラート:
- CPU使用率
- メモリ使用率
- エラーレート
- レスポンスタイムAWSでのモノリス構成
カオリはAWS構成図を描いた。
# 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"]モノリスの限界
もちろん、モノリスにも限界はある。カオリはそれも正直に認識していた。
スケーリングの粗さ
「注文処理の負荷が高い時に、商品閲覧も遅くなってしまう」
モノリスは全機能が同じプロセスで動くため、特定の機能だけスケールさせることができない。全体を水平スケールするしかない。
デプロイの影響範囲
一箇所のバグが全体に影響する。商品カタログの変更で決済機能がダウンしたら、ビジネスへの影響は甚大だ。
チームの並行開発の摩擦
チームが大きくなると、同じコードベースへの変更が衝突しやすくなる。マージコンフリクトが増え、大きな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が少ない)
✅ 初年度ユーザー数が数十万以下
✅ 開発速度が最優先
マイクロサービス移行を検討する状況:
⚠️ 特定機能だけが極端に高負荷
⚠️ チームが独立してデプロイしたい
⚠️ ドメインの境界が明確になってきた
⚠️ ダウンタイムが許容されない機能がある
「今の私たちは全部✅だ」とカオリは言った。「だからまずはモノリスから始める。ただしモジュラーモノリスとして、将来の分割を意識した境界で設計する」
チームは納得した。これがカオリの最初のアーキテクチャ判断だった。
次章では、モノリス内の設計を整理する「レイヤードアーキテクチャ」を学ぶ。コードを水平な層に分けることで、保守性を高める方法を見ていこう。