code-architecture について
code-architectureは、単一のデプロイ単位内でコードを整理するためのスキルです。新規プロジェクトの設計や既存コードの診断に向けて、レイヤード、ヘキサゴナル、機能別の構成、依存関係やテスト配置を解説します。
主な機能
- 01クリーン/ヘキサゴナルアーキテクチャ
- 02機能別のコード構成
- 03依存性逆転と注入
- 04モジュール境界と公開API
- 05循環依存の診断
- 06アーキテクチャ層ごとのテスト計画
活用シーン
- 01新規プロジェクトのアーキテクチャ選定
- 02成長するアプリのモジュール整理
- 03肥大化したコントローラーや循環依存の診断
- 04ビジネスロジックを分離するテスト構成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill code-architectureインストール後、Claude Code で/code-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.claude-plugin/plugin.jsonreferences/architecture-patterns.mddependency-inversion.md詳細
コードアーキテクチャ
現実に直面しても通用するアーキテクチャパターンとコードの構成です。目標は、書かれてから18か月後も理解し、テストし、変更できるコードです。ここではモジュールおよびアプリケーションレベルでの決定を扱います。サービスの分割方法でも、スキーマの設計方法でもなく、単一のデプロイ可能な単位内でコードをどう構成するかを扱います。
使用するタイミング
次のような場合に使用します:
- 新しいプロジェクトでアーキテクチャパターンを選ぶ(クリーン、ヘキサゴナル、機能ベース、垂直スライス)
- 既存コードのアーキテクチャ上の問題を診断する(肥大化したコントローラー、循環依存、絡み合ったビジネスロジック)
- 依存性逆転を適用して、密結合したコードを整理する
- フォルダー構造とモジュール構成を決める
- モジュール規模でSOLID原則を適用する(クラス規模だけでなく)
- 依存性注入コンテナーを設定する
- モジュール境界とバレルエクスポートを定義する
- アーキテクチャのレイヤーに応じてテストピラミッドを設計する
次の用途には使用しません:
- モノリスをマイクロサービスに分割する(
microservices-patternsを使用してください) - データベーススキーマの設計、正規化、クエリの最適化(
database-design-patternsを使用してください) - フレームワーク固有のルーティングやミドルウェアの設定
- CI/CDパイプラインのアーキテクチャ
中核となる決定:どのアーキテクチャパターンを選ぶか?
flowchart TD
Start[New project or restructuring?] --> Team{Team size?}
Team -->|Solo / 2 dev| Size{App complexity?}
Team -->|3-8 dev| MedComplex{Domain complexity?}
Team -->|8+ dev| Large[Feature-based / Vertical Slice]
Size -->|Simple CRUD, low domain logic| Simple[Flat + MVC or Feature-Based]
Size -->|Complex domain, long-lived| Hex[Hexagonal Architecture]
MedComplex -->|Low: thin business layer| FeatureBased[Feature-Based Organization]
MedComplex -->|High: rich business rules| Clean[Clean Architecture]
MedComplex -->|Mixed: CRUD + some complex| Vertical[Vertical Slice Architecture]
Simple --> SCheck{Will it grow?}
SCheck -->|No, short-lived| MVC[Layer-based MVC is fine]
SCheck -->|Yes| FeatureBased
Large --> Testing{Testing rigor?}
Testing -->|Low: mostly E2E| Vertical
Testing -->|High: unit + integration| Clean
Clean --> ClNote[Entities → Use Cases → Interface Adapters → Frameworks]
Hex --> HexNote[Domain core ↔ Ports ↔ Adapters]
FeatureBased --> FBNote[features/X contains all layers for X]
Vertical --> VSNote[Each slice owns its full stack top to bottom]
クリーンアーキテクチャのレイヤー
古典的なオニオン型です。各リングは内側のみに依存し、外側には依存しません。
graph TB
subgraph Frameworks ["Frameworks & Drivers (outer)"]
DB[(Database)]
HTTP[HTTP Server]
UI[UI / CLI]
External[External APIs]
end
subgraph Adapters ["Interface Adapters"]
Controllers[Controllers / Routes]
Presenters[Presenters]
Gateways[Repository Implementations]
DTOs[DTOs / View Models]
end
subgraph UseCases ["Use Cases (Application Layer)"]
UC1[CreateOrder]
UC2[ProcessPayment]
UC3[NotifyUser]
Ports[Repository Interfaces]
end
subgraph Entities ["Entities (Domain)"]
Order[Order]
User[User]
Payment[Payment]
Rules[Business Rules]
end
HTTP --> Controllers
Controllers --> UC1
UC1 --> Ports
Gateways --> Ports
DB --> Gateways
UC1 --> Order
依存関係のルール:ソースコードの依存関係は内側にのみ向かいます。エンティティはユースケースについて何も知りません。ユースケースはコントローラーについて何も知りません。コントローラーはデータベースドライバーについて何も知りません。
各レイヤーに含まれるもの:
| レイヤー | 含まれるもの | 例 |
|---|---|---|
| エンティティ | エンタープライズのビジネスルール | Order、Payment、ドメインイベント、値オブジェクト |
| ユースケース | アプリケーション固有のビジネスロジック | CreateOrderUseCase、RefundPaymentUseCase |
| インターフェースアダプター | ユースケースと外部形式の間でデータを変換するもの | コントローラー、プレゼンター、リポジトリー実装 |
| フレームワーク | 外部ツール、データベース、UIフレームワーク | Express、Postgres、React |
ヘキサゴナルアーキテクチャ(ポートとアダプター)
多くのチームにとって、クリーンアーキテクチャよりもシンプルな考え方です。すべてがポート(インターフェース)を介して接続され、アダプターがそれを実装します。
graph LR
subgraph Domain ["Domain (Hexagon)"]
Logic[Business Logic]
DPorts[Driven Ports<br/>interfaces the domain calls]
end
subgraph Driving ["Driving Adapters<br/>(what drives the app)"]
HTTP2[HTTP Controller]
CLI2[CLI Command]
Test[Test Harness]
end
subgraph Driven ["Driven Adapters<br/>(what the app drives)"]
DB2[(PostgreSQL Adapter)]
Email[SendGrid Adapter]
Cache[Redis Adapter]
end
HTTP2 -->|calls| Logic
CLI2 -->|calls| Logic
Test -->|calls| Logic
Logic -->|calls port| DPorts
DPorts -.->|implemented by| DB2
DPorts -.->|implemented by| Email
DPorts -.->|implemented by| Cache
ヘキサゴナルがクリーンアーキテクチャより適している場合:インフラを簡単に差し替える必要がある場合(テストではインメモリー、本番環境ではPostgresを使用するなど)、またはビジネスロジックが十分に豊富で分離する価値はあるものの、クリーンアーキテクチャのような完全なレイヤー分割までは必要ない場合です。
機能ベースとレイヤーベースの構成
レイヤーベース(従来のMVC)
src/
controllers/
user.controller.ts
order.controller.ts
payment.controller.ts
services/
user.service.ts
order.service.ts
payment.service.ts
repositories/
user.repository.ts
order.repository.ts
models/
user.model.ts
order.model.ts
問題点:「orders」機能を追加または変更するには、4つのディレクトリーにあるファイルを編集する必要があります。ordersドメインを理解するには、フォルダー間を行き来する必要があります。
機能ベース(成長中のアプリに推奨)
src/
features/
orders/
orders.controller.ts
orders.service.ts
orders.repository.ts
orders.schema.ts
orders.types.ts
orders.test.ts
index.ts ← public API (barrel export)
users/
users.controller.ts
users.service.ts
...
index.ts
payments/
...
shared/
database/
logger/
config/
利点:注文に関するすべてのコードが同じ場所にまとまります。機能を削除するには、フォルダーを1つ削除するだけです。開発者がordersドメインについて理解するには、1つのディレクトリーを見るだけで済みます。
ルール:機能がインポートできるのはshared/と、他の機能の公開されたindex.tsのみです。互いの内部には決して立ち入りません。
アンチパターン:コントローラーにビジネスロジックを置く(肥大化したコントローラー)
初心者:「リクエストが入ってくる場所がコントローラーだから、ロジックもそこに置きます。便利です。」
// Fat controller — don't do this
app.post('/orders', async (req, res) => {
const { userId, items } = req.body;
// Business logic #1: calculate total
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
// Business logic #2: apply discount
const user = await db.users.findById(userId);
const discount = user.memberSince < oneYearAgo ? 0.1 : 0;
const finalTotal = total * (1 - discount);
// Business logic #3: check inventory
for (const item of items) {
const stock = await db.inventory.findById(item.productId);
if (stock.quantity < item.qty) {
return res.status(409).json({ error: 'Out of stock' });
}
}
// Side effects mixed in
const order = await db.orders.create({ userId, items, total: finalTotal });
await emailService.send(user.email, 'order-confirmation', { order });
await inventory.decrement(items);
res.json(order);
});
エキスパート:コントローラーはHTTPアダプターです。HTTPからドメインへ変換し、ユースケースまたはサービスを呼び出してから、ドメインからHTTPレスポンスへ変換します。すべてのビジネスロジックは、HTTPコンテキストなしでテストできるユースケースまたはドメインサービスに置くべきです。
// Lean controller
app.post('/orders', async (req, res) => {
try {
const command = CreateOrderCommand.fromRequest(req.body); // Validate + map
const order = await createOrderUseCase.execute(command); // All logic here
res.status(201).json(OrderPresenter.toJSON(order)); // Map to response
} catch (error) {
errorHandler(res, error);
}
});
// Use case: testable, framework-agnostic
class CreateOrderUseCase {
constructor(
private readonly orderRepo: OrderRepository,
private readonly inventoryService: InventoryService,
private readonly discountPolicy: DiscountPolicy,
private readonly notifications: NotificationPort,
) {}
async execute(command: CreateOrderCommand): Promise<Order> {
const user = await this.orderRepo.findUser(command.userId);
const discount = this.discountPolicy.calculate(user);
await this.inventoryService.reserveItems(command.items);
const order = Order.create(command.items, discount);
await this.orderRepo.save(order);
await this.notifications.orderCreated(order, user);
return order;
}
}
検出方法:ロジックが約20行を超えるコントローラー、データベースモデルを直接インポートするコントローラー、ネストしたif-elseのビジネス条件を含むコントローラーです。
アンチパターン:アーキテクチャの宇宙飛行士(抽象化のための抽象化)
初心者:「Repository インターフェース、Repository の実装、Service、ServiceInterface、Service を作成する Factory、EventBus、CQRS のコマンドハンドラーを追加します。これでエンタープライズ品質です。」
エキスパート:抽象化にはすべてコストがかかります。ファイルが増え、間接性が高まり、オンボーディングが難しくなり、保守するものも増えます。抽象化は、テスト、差し替え、コード再利用を可能にするときに効果を発揮する投資です。変更されることのない単一の実装しかない UserServiceInterface を追加するなら、抽象化のコストを払うだけで、そのメリットは得られません。
次のように問いかけてください:「この抽象化によって、ほかの方法では実現できない何が可能になるのか?」
- Repository インターフェース → テストで実際の DB をインメモリのものに差し替えられます。すぐに効果があります。
- Service インターフェース → Service が常に 1 つしかないなら、これは形式的な手続きです。
- Factory パターン → オブジェクトの生成が複雑な場合や、複数の戦略がある場合に効果があります。
- イベントバス → 多くのコンポーネントが互いを知らずにドメインイベントに反応する必要がある場合に効果があります。
検出方法:*Interface.ts、*Abstract.ts、*Factory.ts という名前のファイルがあり、実装する側も呼び出す側も 1 つだけで、その実装が変わらない場合です。
変遷:Enterprise Java(2005~2015 年)では、あらゆるものを抽象化するのが標準でした。Spring Framework はこれを推奨しました。2015 年以降の Node.js と Go のコミュニティは、「退屈なテクノロジー」の原則を掲げてこれに反発しました。2026 年において、適切な抽象化のレベルは状況によって異なります。抽象化をまったくしないのも、最大限に行うのも適切ではありません。
アンチパターン:循環依存
初心者:「Order モジュールは Users について知る必要があり、User モジュールはユーザーの注文を確認する必要があります。だから、互いにインポートします。」
// orders/order.service.ts
import { UserService } from '../users/user.service'; // Order → User
// users/user.service.ts
import { OrderService } from '../orders/order.service'; // User → Order
// Node.js will silently give you `undefined` at runtime
// Jest will give you cryptic "Cannot access before initialization" errors
エキスパート:循環依存は、ドメインモデリングに問題があることを示しています。本当に互いを必要とする 2 つのモジュールは、1 つのモジュールに統合するか、両方が依存する 3 つ目のモジュールを共有するか、イベントやインターフェースを介して通信する必要があります。
解決策:
- 統合する:Order と User が本当に切り離せないなら、
accounts/にまとめます - 共通部分を抽出する:両方からインポートできる
order-summary/を作成します - インターフェースで依存関係を逆転する:User モジュールで
OrderSummaryPortインターフェースを定義し、Orders が実装します。User が Orders をインポートすることはありません - イベント駆動にする:User が OrderService を直接呼び出すのではなく、
OrderCreatedイベントに反応します
# Detect circular deps
npx madge --circular src/
npx dpdm --circular src/index.ts
# ESLint rule (add to .eslintrc)
# "import/no-cycle": "error"
検出方法:起動時にモジュールの値が undefined になるランタイムエラー、ESLint の import/no-cycle 違反、madge の循環依存出力です。
実践での依存関係逆転
SOLID の D は、具象ではなく抽象に依存するという原則です。モジュール規模で適用すると、次のようになります。
// Bad: Use case is coupled to Postgres
class CreateOrderUseCase {
constructor(private readonly db: PostgresConnection) {}
async execute(cmd: CreateOrderCommand) {
await this.db.query('INSERT INTO orders ...');
}
}
// Good: Use case depends on an interface
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: OrderId): Promise<Order | null>;
}
class CreateOrderUseCase {
constructor(private readonly orderRepo: OrderRepository) {}
async execute(cmd: CreateOrderCommand) {
const order = Order.create(cmd);
await this.orderRepo.save(order); // No SQL, no Postgres, no coupling
}
}
// Production: Postgres implements the interface
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> { /* Postgres SQL */ }
async findById(id: OrderId): Promise<Order | null> { /* Postgres SQL */ }
}
// Tests: in-memory implements the same interface
class InMemoryOrderRepository implements OrderRepository {
private orders = new Map<string, Order>();
async save(order: Order) { this.orders.set(order.id, order); }
async findById(id: OrderId) { return this.orders.get(id) ?? null; }
}
DI コンテナーが有効な場合:配線が必要な依存関係が多数あり、手作業での配線がエラーを招きやすい、または繰り返しが多い場合です。NestJS、InversifyJS(TypeScript)、Python の dependency-injector、Spring(Java)が該当します。
DI コンテナーが過剰な場合:シンプルなスクリプト、依存関係の少ない小規模サービス、サーバーレス関数、Go プロジェクト(コンストラクターインジェクションが慣用的で十分です)が該当します。
モジュール境界とバレルエクスポート
各機能/モジュールは、index.ts を介して公開 API を提供する必要があります。
// features/orders/index.ts — public API
export { CreateOrderUseCase } from './create-order.use-case';
export { OrderRepository } from './order.repository.interface';
export type { Order, OrderStatus } from './order.entity';
// NOT exported: internal helpers, SQL queries, implementation details
// Other modules import from the public API only
import { CreateOrderUseCase } from '@/features/orders';
// NOT: import from '@/features/orders/create-order.use-case'
ESLint で強制する:
// .eslintrc
{
"rules": {
"import/no-internal-modules": ["error", {
"allow": ["**/*.test.ts", "**/index.ts"]
}]
}
}
テストアーキテクチャ(テストピラミッドでの配置)
各アーキテクチャ層には、適したテストの種類があります。
| 層 | テストの種類 | 速度 | カバレッジ |
|---|---|---|---|
| エンティティ/ドメイン | 単体テスト | 瞬時 | 90%+ |
| ユースケース/アプリケーション | モックを使った単体テスト | 高速 | 80%+ |
| インターフェースアダプター | 統合テスト | 中速 | 70%+ |
| フレームワーク/外部 | E2E/コントラクトテスト | 低速 | 主要なパス |
原則:ビジネスロジックのテストでは、サーバーの起動、データベースへの接続、ネットワーク呼び出しを必要としないようにしてください。ユースケースのテストで実際のデータベースが必要なら、アーキテクチャによってインフラストラクチャ上の関心事がドメインに漏れています。
参考資料
references/architecture-patterns.md— 詳細なパターンについては、クリーンアーキテクチャのレイヤー、ヘキサゴナルアーキテクチャのポートとアダプター、垂直スライス、実例を含む機能ベースの構成を参照してくださいreferences/dependency-inversion.md— TypeScript と Python の IoC コンテナー、コンストラクターインジェクションのパターン、DI が過剰となる場合、一般的な DI のアンチパターンについては、こちらを参照してください