system-architecture について
モジュール、サービス、大規模機能の構造を設計する開発者向けのスキルです。Python/Reactのレイヤー構成やデータベース、状態管理の判断を支援し、architecture.mdとADRに記録します。
主な機能
- 01レイヤー構成と依存関係のルール
- 02アーキテクチャ案の評価手順
- 03データベース設計と移行の指針
- 04Reactの状態管理の指針
- 05設計ドキュメントとADRの作成
活用シーン
- 01新しいモジュールやサブシステムの計画
- 02データベーススキーマ変更の評価
- 03フロントエンド状態管理の選定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hieutrtr/ ai1-skills --skill system-architectureインストール後、Claude Code で/system-architectureを入力して読み込まれたか確認します。
同梱ファイルreferences/architecture-decision-record-template.mdreferences/layer-responsibilities.md
詳細
システムアーキテクチャ
使用する場面
次の場合にこのスキルを使用します:
- 構造上の決定が必要な新しいモジュール、サービス、または大規模な機能を設計するとき
- アーキテクチャのアプローチを選択するとき(例:ロジックの配置場所、データフローの構成方法)
- データベーススキーマの変更を計画するとき、または既存のスキーマをリファクタリングするとき
- フロントエンドの状態管理を決定するとき(サーバー状態とクライアント状態、コンテキストとストア)
- 新しい機能の技術的なトレードオフを評価するとき
- アーキテクチャ決定記録(ADR)を作成またはレビューするとき
- 新しいプロジェクトや大規模なサブシステムをゼロから立ち上げるとき
入力: plan.md(project-plannerが作成)が存在する場合は、機能の範囲と影響を受けるモジュールについての背景情報を得るために読みます。それ以外の場合は、ユーザーのリクエストを直接もとに作業します。
出力: アーキテクチャの決定事項をarchitecture.mdに記述し、ADRをdocs/adr/ADR-NNN-<title>.mdに作成します。ユーザーには「アーキテクチャをarchitecture.mdに記述しました。API コントラクトについては/api-design-patternsを、実装タスクについては/task-decompositionを実行してください。」と伝えます。
次の場合、このスキルは使用しないでください:
- 実装コードを書く場合(
python-backend-expertまたはreact-frontend-expertを使用) - API コントラクトの設計やエンドポイント仕様を作成する場合(
api-design-patternsを使用) - テストのパターンや戦略を作成する場合(
pytest-patternsまたはreact-testing-patternsを使用) - デプロイやインフラストラクチャに関する決定を行う場合(
docker-best-practicesまたはdeployment-pipelineを使用)
手順
プロジェクトのレイヤーアーキテクチャ
標準的なPython/Reactフルスタックアーキテクチャでは、依存関係の方向を厳密に定めたレイヤーパターンに従います。
バックエンドレイヤー(FastAPI)
HTTP Request
↓
┌─────────────────────┐
│ Routers (routes/) │ ← HTTP concerns: request parsing, response formatting, status codes
│ │ Uses: Depends() for injection, Pydantic schemas for validation
├─────────────────────┤
│ Services │ ← Business logic: orchestration, validation rules, domain operations
│ (services/) │ No HTTP awareness. Raises domain exceptions, not HTTPException.
├─────────────────────┤
│ Repositories │ ← Data access: queries, CRUD operations, database interactions
│ (repositories/) │ No business logic. Returns model instances or None.
├─────────────────────┤
│ Models (models/) │ ← SQLAlchemy ORM models: table definitions, relationships, indexes
│ Schemas (schemas/) │ ← Pydantic v2 models: request/response contracts, validation
└─────────────────────┘
↓
Database
依存関係の方向に関するルール:
- ルーターはサービスに依存します(リポジトリに直接依存してはいけません)
- サービスはリポジトリに依存します(ルーターに依存してはいけません)
- リポジトリはモデルに依存します(サービスに依存してはいけません)
- スキーマはレイヤー間で共有しますが、それ自体は依存関係を定義しません
- レイヤーを飛ばしてはいけません。ルートからデータベースに直接アクセスしてはいけません
依存性注入のパターン:
# Router depends on Service via Depends()
@router.post("/users", response_model=UserResponse)
async def create_user(
data: UserCreate,
service: UserService = Depends(get_user_service),
) -> UserResponse:
return await service.create_user(data)
# Service depends on Repository via constructor injection
class UserService:
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
# Repository depends on AsyncSession via Depends()
class UserRepository:
def __init__(self, session: AsyncSession) -> None:
self.session = session
フロントエンドレイヤー(React/TypeScript)
┌─────────────────────┐
│ Pages (pages/) │ ← Route-level components: data fetching, layout composition
├─────────────────────┤
│ Layouts │ ← Page structure: navigation, sidebars, content areas
│ (layouts/) │
├─────────────────────┤
│ Features │ ← Domain-specific: UserProfile, OrderList, ChatPanel
│ (features/) │ Composed from shared components + hooks
├─────────────────────┤
│ Shared Components │ ← Reusable UI: Button, Modal, Table, Form, Input
│ (components/) │ No business logic. Configurable via props.
├─────────────────────┤
│ Hooks (hooks/) │ ← Custom hooks: useAuth, usePagination, useDebounce
│ API (api/) │ ← API client functions, TanStack Query configurations
├─────────────────────┤
│ Types (types/) │ ← Shared TypeScript interfaces and type definitions
└─────────────────────┘
コンポーネントの依存関係の方向:
- ページは機能とレイアウトをインポートします
- 機能は共有コンポーネントとフックをインポートします
- 共有コンポーネントは、ほかの共有コンポーネントと型のみをインポートします
- フックはAPI関数と型をインポートします
- API関数は型のみをインポートします
意思決定のフレームワーク
アーキテクチャ上の決定に直面した場合は、次の構造化されたプロセスに従います。
ステップ1:問題を定義する
- 必要な機能は何ですか?
- 非機能要件は何ですか?(パフォーマンス、スケーラビリティ、保守性)
- どのような制約がありますか?(チームの規模、スケジュール、既存のインフラストラクチャ)
ステップ2:選択肢を特定する
- 実現可能なアーキテクチャのアプローチを2~3個挙げます
- 各選択肢について、次の内容を記録します:
- 仕組み(簡潔な技術的説明)
- 利点
- 欠点
- リスク
ステップ3:評価基準に照らして評価する
| 基準 | 重要度 | 説明 |
|---|---|---|
| 保守性 | 高 | チームが簡単に理解、変更、デバッグできますか? |
| テスト容易性 | 高 | 各コンポーネントを個別にテストできますか? |
| パフォーマンス | 中 | レイテンシとスループットの要件を満たしますか? |
| チームの習熟度 | 中 | チームにこのアプローチの経験がありますか? |
| 運用コスト | 低 | インフラストラクチャと保守にどの程度のコストがかかりますか? |
| 将来の柔軟性 | 低 | 要件の変更に応じて、どの程度容易に進化させられますか? |
ステップ4:決定して記録する
- 重み付けされた基準を最もよく満たす選択肢を選びます
- ADRに決定事項を記録します(
references/architecture-decision-record-template.mdを参照) - 選択しなかった内容とその理由を記録します。この背景情報は今後の決定に役立ちます
ステップ5:共有する
- ADRをチームと共有します
- 必要な移行手順や段階的な展開手順を特定します
- 元に戻せるかどうかを明確にします。一方通行の決定ですか、それとも双方向の決定ですか?
データベーススキーマの設計
設計原則
- 正規化から始める(3NF) — 非正規化は、推測ではなく、パフォーマンスのボトルネックが実証された場合にのみ行います
- 論理的な変更ごとにマイグレーションを1つ作成する — 各 Alembic マイグレーションは、単一の一貫したスキーマ変更を表す必要があります
- 必ずダウングレードを含める — すべてのマイグレーションに、正常に動作する
downgrade()関数が必要です - 戦略的にインデックスを設定する:
- 主キー(自動)
- 外部キー(常に)
- 頻繁に実行されるクエリの WHERE 句で使用するカラム
- 複数カラムの検索用の複合インデックス
- 絞り込みクエリ用の部分インデックス(例:
WHERE is_active = true)
SQLAlchemy 2.0 の非同期パターン
# Model definition with Mapped types (SQLAlchemy 2.0 style)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
# Relationships: ALWAYS use eager loading with async
posts: Mapped[list["Post"]] = relationship(
back_populates="author",
lazy="selectin", # or "joined" — NEVER "lazy" with async
)
非同期セッションのルール:
- リクエストごとに
AsyncSessionを1つ使用します — 並行タスク間で共有してはいけません - 自動クリーンアップのため、
async withコンテキストマネージャーを使用します - セッションの境界をトランザクションの境界に合わせます
selectinまたはjoinedによるロードを使用します — 遅延ロードは asyncio と互換性がありませんrun_sync()はレガシーコード向けの最終手段としてのみ使用します
マイグレーション計画
- スキーマを変更 → マイグレーションを生成:
alembic revision --autogenerate -m "description" - 生成されたマイグレーションをレビュー — カラムの型、インデックス、制約を確認します
- アップグレードをテスト:
alembic upgrade head - ダウングレードをテスト:
alembic downgrade -1 - データの保持をテスト:既存のデータが一連の変更の前後で保持されることを確認します
フロントエンドアーキテクチャ
状態管理の判断フロー
Is the data from the server?
├── YES → Use TanStack Query (useQuery, useMutation)
│ Configure staleTime, gcTime, query keys
│
└── NO → Is it needed across multiple components?
├── YES → Is it complex with actions/reducers?
│ ├── YES → Use Zustand store
│ └── NO → Use React Context
│
└── NO → Use useState / useReducer locally
TanStack Query の規約:
- クエリキー:
[resource, ...identifiers](例:["users", userId]、["posts", { page, limit }]) queryOptions()ファクトリーを使ってキーと関数の定義を一元化します — コピー&ペーストによるキーの誤りを防げます- データの鮮度要件に応じて
staleTimeを設定します(デフォルトの 0 は、ほとんどの場合、頻繁に更新されすぎます) - ミューテーションの後は
invalidateQueries()で無効化します — 手動でrefetch()を呼び出してはいけません - すべての状態を処理します:
isPending、isError、data
コンポーネント設計のルール:
- 設定には Props、データにはフックを使用します
- 状態は必要な範囲でのみ持ち上げます — 早まってコンテキストを作成してはいけません
- コンポーネントは200行未満に保ちます — それを超える場合は、サブコンポーネントまたはカスタムフックに切り出します
- 深い階層での Props の受け渡しよりも、
childrenとコンポジションを使用します
ルーティング構造
URL 構造を反映するようにルートを整理します:
src/
├── pages/
│ ├── HomePage.tsx → /
│ ├── LoginPage.tsx → /login
│ ├── users/
│ │ ├── UserListPage.tsx → /users
│ │ └── UserDetailPage.tsx → /users/:id
│ └── settings/
│ └── SettingsPage.tsx → /settings
横断的な関心事
認証フロー
Login Request
↓
Backend: Validate credentials → Generate JWT (access + refresh tokens)
↓
Frontend: Store access token in memory, refresh token in httpOnly cookie
↓
API Calls: Attach access token via Authorization header
↓
Token Expired: Use refresh token to obtain new access token
↓
Refresh Failed: Redirect to login
認証に関するアーキテクチャ上の決定:
- アクセストークン:有効期間を短くし(15~30分)、メモリに保存します(localStorage には保存しません)
- リフレッシュトークン:有効期間を長くし(7~30日)、httpOnly Cookie に保存します
- バックエンド:トークンの検証 → ユーザーの抽出 → 権限の確認には、FastAPI の
Depends()チェーンを使用します - フロントエンド:
user、login()、logout()、isAuthenticatedを提供する認証コンテキストを使用します
エラー処理の方針
エラーは適切なレイヤーで処理します:
| レイヤー | エラーの種類 | 対応 |
|---|---|---|
| ルーター | HTTPException | ステータスコードを含む HTTP エラーレスポンスを返します |
| サービス | ドメイン例外 | カスタム例外(例:UserNotFoundError)を送出します |
| リポジトリ | データベース例外 | ドメイン例外として捕捉して再送出するか、そのまま伝播させます |
| フロントエンド | API エラー | ユーザーにわかりやすいメッセージを表示し、適切な場合は再試行します |
バックエンドの例外階層:
class AppError(Exception):
"""Base application error."""
class NotFoundError(AppError):
"""Resource not found."""
class ConflictError(AppError):
"""Resource conflict (duplicate, version mismatch)."""
class ValidationError(AppError):
"""Business rule violation."""
ルーターレベルの例外ハンドラーは、ドメイン例外を HTTP レスポンスにマッピングします:
@app.exception_handler(NotFoundError)
async def not_found_handler(request: Request, exc: NotFoundError):
return JSONResponse(status_code=404, content={"detail": str(exc)})
ロギングアーキテクチャ
バックエンド(structlog):
- 本番環境では構造化 JSON ログを使用します
- 開発環境では人が読みやすいコンソール出力を使用します
- ミドルウェアレベルでリクエストコンテキスト(request_id、user_id)をバインドします
- リポジトリレイヤーではなく、サービスレイヤー(ビジネスイベント)でログを記録します(リポジトリレイヤーではログが過剰になります)
- ログレベルを使用します:DEBUG(開発時のみ)、INFO(ビジネスイベント)、WARNING(回復可能な問題)、ERROR(対応が必要な失敗)
フロントエンド:
- 開発時は
console.*を使用します - 本番環境では、構造化されたエラー情報をバックエンドまたは Sentry に送信します
- デバッグ用にユーザーの操作を記録し、分析目的では記録しません
設定管理
バックエンド(pydantic-settings):
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
database_url: str
redis_url: str = "redis://localhost:6379"
jwt_secret: str
debug: bool = False
フロントエンド(環境変数):
- API のベース URL には
VITE_API_URLを使用します - Vite の
import.meta.envを介してビルド時に注入します - フロントエンドの環境変数にシークレットを含めないでください
出力ファイル
architecture.md
プロジェクトのルートにある architecture.md にアーキテクチャドキュメントを記述します。
# Architecture: [Feature/System Name]
## Overview
[1-2 sentence summary of the architectural approach]
## Layer Structure
[Backend and frontend layer descriptions from this skill's patterns]
## Key Decisions
[Summary of decisions made, with links to ADRs]
## Database Schema
[Entity descriptions, relationships, key indexes]
## Cross-Cutting Concerns
[Auth, error handling, logging approach]
## Next Steps
- Run `/api-design-patterns` to define API contracts
- Run `/task-decomposition` to create implementation tasks
ADR
重要な意思決定ごとに、docs/adr/ に ADR を作成します。
# ADR-NNN: [Decision Title]
## Status
Accepted | Proposed | Superseded
## Context
[Why this decision is needed]
## Decision
[What we decided]
## Consequences
[Positive and negative outcomes]
ADR には連番を付けます(ADR-001、ADR-002 など)。
例
アーキテクチャ上の意思決定:リアルタイム通知
課題: アプリケーションでユーザー向けのリアルタイム通知(新着メッセージ、ステータス更新)が必要です。
評価した選択肢:
| 選択肢 | 長所 | 短所 |
|---|---|---|
| WebSocket | 真の双方向通信、低遅延 | 接続管理が複雑で、スケーリングが困難 |
| Server-Sent Events(SSE) | シンプル、HTTP ベース、自動再接続 | 単方向(サーバー→クライアントのみ)、ブラウザーの接続数に制限あり |
| ポーリング | 実装が最も簡単、どこでも動作 | 遅延が大きく、不要なサーバー負荷が発生 |
決定: このユースケースには WebSocket を採用します。
根拠: 通知には低遅延が求められ、システムには将来的に双方向通信(入力中インジケーター、プレゼンス)も必要になります。SSE は通知だけなら機能しますが、将来的な双方向通信のニーズには別のソリューションが必要になります。ポーリングでは、リアルタイム UX に許容できない遅延が生じます。
アーキテクチャ:
- バックエンド:
ConnectionManagerクラスを備えた FastAPI WebSocket エンドポイント - フロントエンド:自動再接続機能を備えたカスタム
useWebSocketフック - スケーリング:複数インスタンス間でメッセージを配信するための Redis pub/sub
- 永続化:オフラインのユーザー向けに通知をデータベースに保存
- フォールバック:通知履歴と初期読み込み用の REST エンドポイント
ADR の完全な形式については、references/architecture-decision-record-template.md を参照してください。
エッジケース
モノリスとマイクロサービス
10 人未満の開発チームでは、モジュラーモノリスをデフォルトとします。モジュラーモノリスには次の利点があります。
- ネットワークのオーバーヘッドなしで、明確なモジュール境界を設けられる
- モジュールごとのスキーマを備えた共有データベース
- リファクタリングとコードのナビゲーションが容易
- デプロイとデバッグがシンプル
マイクロサービスを検討するのは、次の場合に限ります。
- 特定のコンポーネントを独立してスケーリングする必要がある
- モジュールごとに異なる技術スタックが必要である
- チームの規模が 10 人を超え、担当範囲が明確である
- デプロイの独立性がビジネス要件である
移行パス: モノリス内のモジュール境界は、サービスであるかのように設計します(モジュール間でデータベースに直接アクセスせず、サービスインターフェースを介して通信します)。これにより、必要に応じてマイクロサービスへの切り出しが容易になります。
レイヤーパターンを崩す場合
標準的な CRUD 操作では、Router → Service → Repository の厳格なパターンに従う必要があります。許容される例外は次のとおりです。
- バックグラウンドタスク: ルーターを経由せず、サービスを直接呼び出してもかまいません
- イベントハンドラー: ドメインイベントリスナーは、どのコンテキストからでもサービスを呼び出せます
- CLI コマンド: 管理スクリプトはサービスまたはリポジトリに直接アクセスしてもかまいません
- マイグレーション: データマイグレーションではモデルに直接アクセスしてもかまいません(サービス/リポジトリ層は不要です)
- ヘルスチェック: 簡単な接続確認のために、データベースに直接アクセスしてもかまいません
いずれの場合も、ビジネスロジックはサービス層に置く必要があります。これらの例外はエントリーポイントに関するものであり、ビジネスルールを迂回するものではありません。
アーキテクチャの進化
アーキテクチャを変更する必要がある場合:
- 変更の動機と提案内容を記録した ADR を作成します
- 影響を受けるすべてのモジュールとその依存関係を特定します
- 段階的な移行を計画します。一気に書き換えることは避けてください
- 移行期間中は後方互換性を維持します(ストラングラーフィグパターン)
- 移行を完了し、レガシーコードを削除する期限を設定します