Kotlin: Kotlinプロジェクト設計詳解
最終更新:2026-08-26
これまでの24課がここに集約されます。Charlieは要件分析から始め、OrderProcessorのドメインモデル、レイヤードアーキテクチャ、技術スタック選定、APIコントラクトを設計します。これが実プロジェクトの第一歩です。
1. 学べること
- 要件分析とドメインモデリング:注文ステートマシン、イベントソーシング
- レイヤードアーキテクチャ:Controller → Service → Repository → Domain
- 技術スタック:Spring Boot + Coroutines + kotlinx.serialization + Ktor Client
- API設計:RESTfulエンドポイント計画 + OpenAPIドキュメント
- Charlieの実践:OrderProcessorアーキテクチャ図とデータベーススキーマ
2. 本物の建築家の物語
(1) 課題:設計なしのコーディングは手戻りを保証する
Charlieのチームはかつて設計を飛ばして直接コーディングを始めました。3ヶ月後、ドメインモデルがビジネス要件と合わず、コードの60%を書き直す必要がありました。損失:3ヶ月の開発 + 2ヶ月の手戻り。
(2) 設計ファーストアプローチ
TEXT
📖 参照専用
Week 1: Requirements → Domain Model → State Machine
Week 2: Architecture → Tech Stack → API Contract
Week 3: Database Schema → Module Boundaries → CI/CD Plan
Week 4+: Development (with clear blueprint)
設計に3週間投資し、手戻りを60%削減。設計なしの「高速開発」は最も遅い道です。
3. 要件分析とドメインモデリング
(1) コア要件
| 要件 | 説明 | 優先度 |
|---|---|---|
| 注文作成 | 顧客が注文、保留注文を生成 | P0 |
| 注文支払い | 支払い成功後、注文を確認 | P0 |
| 注文出荷 | 倉庫が出荷、追跡番号を生成 | P0 |
| 注文キャンセル | 顧客/システムが注文をキャンセル、返金を実行 | P0 |
| 高額ルーティング | 10,000 USD以上の注文はVIPパイプラインへ | P1 |
| バッチ処理 | 5,000注文/秒のスループットをサポート | P1 |
| イベントソーシング | 注文ステータス変更がドメインイベントを生成 | P2 |
(2) ドメインモデル
KOTLIN
// Core domain entities
data class Order(
val id: String,
val total: Double,
var status: OrderStatus,
val customerId: String,
val items: List<OrderItem>,
val createdAt: String,
var updatedAt: String?
)
data class OrderItem(val sku: String, val quantity: Int, val unitPrice: Double)
sealed class OrderStatus {
object Pending : OrderStatus()
data class Processing(val step: Int) : OrderStatus()
object Paid : OrderStatus()
object Shipped : OrderStatus()
object Delivered : OrderStatus()
data class Cancelled(val reason: String) : OrderStatus()
}
sealed class OrderEvent {
data class Created(val orderId: String, val total: Double) : OrderEvent()
data class PaymentReceived(val orderId: String, val amount: Double) : OrderEvent()
data class Shipped(val orderId: String, val trackingCode: String) : OrderEvent()
data class Cancelled(val orderId: String, val reason: String) : OrderEvent()
}
(3) 注文ステートマシン
stateDiagram-v2
[*] --> Pending: Create Order
Pending --> Processing: Start Processing
Pending --> Cancelled: Customer Cancel
Processing --> Paid: Payment Success
Processing --> Cancelled: Payment Failed
Paid --> Shipped: Ship Order
Shipped --> Delivered: Delivery Confirmed
Delivered --> [*]
Cancelled --> [*]
(1) ▶ サンプル
sealed classによるステートマシンの遷移を検証する例です。コンパイラが網羅チェックを強制するため、不正遷移を安全に防げます。
KOTLIN
sealed class TaskStatus {
object Todo : TaskStatus()
object InProgress : TaskStatus()
object Done : TaskStatus()
data class Cancelled(val reason: String) : TaskStatus()
}
fun nextStatus(current: TaskStatus): TaskStatus = when (current) {
is TaskStatus.Todo -> TaskStatus.InProgress
is TaskStatus.InProgress -> TaskStatus.Done
is TaskStatus.Done -> TaskStatus.Done // Terminal state
is TaskStatus.Cancelled -> current // Cannot resume
}
fun main() {
val states = listOf(
TaskStatus.Todo,
TaskStatus.InProgress,
TaskStatus.Done,
TaskStatus.Cancelled("Blocked")
)
states.forEach { s ->
println("$s -> ${nextStatus(s)}")
}
}
出力:
TEXT 📖 参照専用Todo -> InProgress InProgress -> Done Done -> Done Cancelled(reason=Blocked) -> Cancelled(reason=Blocked)
4. レイヤードアーキテクチャ
(1) 4層アーキテクチャ
flowchart TD
A[Controller Layer<br/>REST API / Request Validation] --> B[Service Layer<br/>Business Logic / State Machine]
B --> C[Repository Layer<br/>Data Access / Persistence]
B --> D[Integration Layer<br/>External API Calls]
C --> E[(Database)]
D --> F[Payment Service]
D --> G[Inventory Service]
(2) 各層の責務
| 層 | 責務 | 主要技術 |
|---|---|---|
| Controller | HTTPリクエスト処理、バリデーション、レスポンス | Spring WebFlux, suspend |
| Service | ビジネスロジック、ステートマシン、イベント発行 | Coroutines, sealed class |
| Repository | データ永続化、クエリ | R2DBC, JPA |
| Integration | 外部サービス呼び出し | Ktor Client, Resilience4j |
(3) プロジェクトモジュール構造
TEXT
📖 参照専用
order-processor/
├── build.gradle.kts
├── src/main/kotlin/com/order/
│ ├── Application.kt # Spring Boot main
│ ├── controller/
│ │ └── OrderController.kt # REST endpoints
│ ├── service/
│ │ ├── OrderService.kt # Business logic
│ │ └── OrderStateMachine.kt # State transitions
│ ├── repository/
│ │ ├── OrderRepository.kt # Data access
│ │ └── EventRepository.kt # Event store
│ ├── integration/
│ │ ├── PaymentClient.kt # Payment API
│ │ └── InventoryClient.kt # Inventory API
│ ├── domain/
│ │ ├── Order.kt # Entity
│ │ ├── OrderStatus.kt # Status enum
│ │ └── OrderEvent.kt # Domain events
│ ├── config/
│ │ └── AppConfig.kt # Configuration
│ └── exception/
│ └── OrderExceptions.kt # Custom exceptions
└── src/test/kotlin/com/order/
└── ... # Tests
(1) ▶ サンプル
レイヤードアーキテクチャに従ったシンプルな依存関係の流れを確認する例です。Controller→Service→Repositoryの呼び出しチェーンをシミュレーションします。
KOTLIN
// Domain
data class Order(val id: String, val total: Double, val status: String)
// Repository Layer
class OrderRepository {
private val db = mutableListOf<Order>()
fun save(order: Order) { db.add(order) }
fun findById(id: String): Order? = db.find { it.id == id }
}
// Service Layer
class OrderService(private val repo: OrderRepository) {
fun createOrder(id: String, total: Double): Order {
val order = Order(id, total, "PENDING")
repo.save(order)
return order
}
fun getOrder(id: String): Order? = repo.findById(id)
}
// Controller Layer
class OrderController(private val service: OrderService) {
fun handleCreate(id: String, total: Double): String {
val order = service.createOrder(id, total)
return "Created: ${order.id} status=${order.status}"
}
}
fun main() {
val repo = OrderRepository()
val service = OrderService(repo)
val controller = OrderController(service)
println(controller.handleCreate("ORD-001", 199.99))
println(service.getOrder("ORD-001"))
}
出力:
TEXT 📖 参照専用Created: ORD-001 status=PENDING Order(id=ORD-001, total=199.99, status=PENDING)
5. 技術スタック選定
| 分野 | 技術選定 | 理由 |
|---|---|---|
| フレームワーク | Spring Boot 3.2 + WebFlux | 成熟したエコシステム、コルーチン対応 |
| 言語 | Kotlin 1.9 | null安全性、コルーチン、data class |
| 非同期 | kotlinx.coroutines | 構造化並行性、suspend |
| シリアライズ | kotlinx.serialization | コンパイル時安全性、マルチフォーマット |
| データベース | PostgreSQL + R2DBC | リアクティブドライバ、コルーチンフレンドリー |
| HTTPクライアント | Ktor Client | Kotlinネイティブ、コルーチン対応 |
| キャッシュ | Redis + kotlinx.coroutines | 非同期キャッシュ |
| テスト | JUnit 5 + MockK | Kotlinネイティブモッキング |
| 監視 | Micrometer + Prometheus | メトリクス収集 |
6. API設計
(1) RESTfulエンドポイント
KOTLIN
// API Endpoints
// POST /api/v1/orders - Create order
// GET /api/v1/orders - List orders (with pagination)
// GET /api/v1/orders/{id} - Get order by ID
// PATCH /api/v1/orders/{id}/status - Update order status
// DELETE /api/v1/orders/{id} - Cancel order
// GET /api/v1/orders/{id}/events - Get order event history
(2) リクエスト/レスポンスDTO
KOTLIN
// Request
data class CreateOrderRequest(
val customerId: String,
val items: List<CreateOrderItemRequest>,
val shippingAddress: AddressRequest?
)
data class CreateOrderItemRequest(
val sku: String,
val quantity: Int,
val unitPrice: Double
)
// Response
data class OrderResponse(
val id: String,
val total: Double,
val status: String,
val customerId: String,
val items: List<OrderItemResponse>,
val createdAt: String,
val updatedAt: String?
)
// Error
data class ErrorResponse(
val code: String,
val message: String,
val details: Map<String, String>? = null
)
(3) API設計原則
| 原則 | 実践 |
|---|---|
| RESTful | リソース名詞 + HTTPメソッドのセマンティクス |
| バージョニング | /api/v1/プレフィックス |
| ページネーション | ?page=0&size=20 |
| フィルタリング | ?status=CONFIRMED&customer=C001 |
| HATEOAS | レスポンスに関連リンクを含める(オプション) |
| エラーフォーマット | 統一されたErrorResponse |
(1) ▶ サンプル
RESTful APIのリクエスト/レスポンスDTOを用いたバリデーション例です。不正リクエストを統一エラーフォーマットで返すパターンを示します。
KOTLIN
data class CreateOrderRequest(
val customerId: String,
val items: List<OrderItemRequest>
)
data class OrderItemRequest(
val sku: String,
val quantity: Int,
val unitPrice: Double
)
data class OrderResponse(
val id: String,
val total: Double,
val status: String
)
data class ErrorResponse(
val code: String,
val message: String
)
fun handleCreate(req: CreateOrderRequest): Any {
// Validate request
if (req.customerId.isBlank()) {
return ErrorResponse("VAL_001", "customerId is required")
}
if (req.items.isEmpty()) {
return ErrorResponse("VAL_002", "At least one item is required")
}
val total = req.items.sumOf { it.quantity * it.unitPrice }
return OrderResponse("ORD-${req.customerId.hashCode().toString().take(3)}", total, "PENDING")
}
fun main() {
val valid = CreateOrderRequest("C001", listOf(OrderItemRequest("SKU-A", 2, 49.99)))
val invalid = CreateOrderRequest("", emptyList())
println(handleCreate(valid))
println(handleCreate(invalid))
}
出力:
TEXT 📖 参照専用OrderResponse(id=ORD-204, total=99.98, status=PENDING) ErrorResponse(code=VAL_001, message=customerId is required)
7. データベーススキーマ
SQL
CREATE TABLE orders (
id VARCHAR(20) PRIMARY KEY,
customer_id VARCHAR(20) NOT NULL,
total DECIMAL(12,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id VARCHAR(20) NOT NULL REFERENCES orders(id),
sku VARCHAR(50) NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL
);
CREATE TABLE order_events (
id SERIAL PRIMARY KEY,
order_id VARCHAR(20) NOT NULL,
event_type VARCHAR(30) NOT NULL,
payload JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_events_order ON order_events(order_id);
8. 完成例:OrderProcessorアーキテクチャ設計ドキュメント
KOTLIN
// ============================================
// OrderProcessor - Architecture Design
// Feature: Complete architecture blueprint
// ============================================
// --- Domain Layer ---
data class OrderItem(val sku: String, val qty: Int, val unitPrice: Double) {
val subtotal: Double get() = qty * unitPrice
}
sealed class OrderStatus {
object Pending : OrderStatus()
object Confirmed : OrderStatus()
object Shipped : OrderStatus()
object Delivered : OrderStatus()
data class Cancelled(val reason: String) : OrderStatus()
}
sealed class OrderEvent {
data class Created(val orderId: String, val total: Double, val customerId: String) : OrderEvent()
data class Confirmed(val orderId: String) : OrderEvent()
data class Shipped(val orderId: String, val trackingCode: String) : OrderEvent()
data class Delivered(val orderId: String) : OrderEvent()
data class Cancelled(val orderId: String, val reason: String) : OrderEvent()
}
data class Order(
val id: String,
val total: Double,
var status: OrderStatus,
val customerId: String,
val items: List<OrderItem>
)
// --- State Machine ---
object OrderStateMachine {
fun transition(current: OrderStatus, event: OrderEvent): OrderStatus = when {
current is OrderStatus.Pending && event is OrderEvent.Created -> OrderStatus.Pending
current is OrderStatus.Pending && event is OrderEvent.Confirmed -> OrderStatus.Confirmed
current is OrderStatus.Confirmed && event is OrderEvent.Shipped -> OrderStatus.Shipped
current is OrderStatus.Shipped && event is OrderEvent.Delivered -> OrderStatus.Delivered
current is OrderStatus.Pending && event is OrderEvent.Cancelled -> OrderStatus.Cancelled(event.reason)
current is OrderStatus.Confirmed && event is OrderEvent.Cancelled -> OrderStatus.Cancelled(event.reason)
else -> throw IllegalStateException("Invalid transition: $current + $event")
}
}
// --- Architecture Summary ---
fun main() {
println("=== OrderProcessor Architecture Design ===\n")
println("Tech Stack:")
println(" Framework: Spring Boot 3.2 + WebFlux")
println(" Language: Kotlin 1.9")
println(" Async: kotlinx.coroutines")
println(" Database: PostgreSQL + R2DBC")
println(" Cache: Redis")
println(" HTTP: Ktor Client")
println(" Testing: JUnit 5 + MockK")
println(" Monitoring: Micrometer + Prometheus")
println("\nOrder Status Machine:")
val order = Order("ORD-001", 299.99, OrderStatus.Pending, "CUST-001", emptyList())
val confirmed = OrderStateMachine.transition(order.status, OrderEvent.Confirmed("ORD-001"))
println(" Pending + Confirmed = $confirmed")
val shipped = OrderStateMachine.transition(confirmed, OrderEvent.Shipped("ORD-001", "TRK-ABC"))
println(" Confirmed + Shipped = $shipped")
val delivered = OrderStateMachine.transition(shipped, OrderEvent.Delivered("ORD-001"))
println(" Shipped + Delivered = $delivered")
println("\nAPI Endpoints:")
println(" POST /api/v1/orders - Create order")
println(" GET /api/v1/orders - List orders")
println(" GET /api/v1/orders/{id} - Get order")
println(" PATCH /api/v1/orders/{id}/status - Update status")
println(" DELETE /api/v1/orders/{id} - Cancel order")
println(" GET /api/v1/orders/{id}/events - Event history")
}
出力:
TEXT
📖 参照専用
=== OrderProcessor Architecture Design ===
Tech Stack:
Framework: Spring Boot 3.2 + WebFlux
Language: Kotlin 1.9
Async: kotlinx.coroutines
Database: PostgreSQL + R2DBC
Cache: Redis
HTTP: Ktor Client
Testing: JUnit 5 + MockK
Monitoring: Micrometer + Prometheus
Order Status Machine:
Pending + Confirmed = Confirmed
Confirmed + Shipped = Shipped
Shipped + Delivered = Delivered
API Endpoints:
POST /api/v1/orders - Create order
GET /api/v1/orders - List orders
GET /api/v1/orders/{id} - Get order
PATCH /api/v1/orders/{id}/status - Update status
DELETE /api/v1/orders/{id} - Cancel order
GET /api/v1/orders/{id}/events - Event history
❓ よくある質問
Q データベースとAPI、どちらを先に設計すべき?
A ドメインモデルを先に設計します(DDDアプローチ)。APIとデータベースはどちらもドメインモデルの投影です。ドメインモデルがコアであり、APIとスキーマは周辺です。
Q イベントソーシングとCRUDの違いは?
A CRUDは現在の状態のみを保存します。イベントソーシングはすべての状態変更イベントを保存します。イベントソーシングは監査、リプレイ、タイムトラベルをサポートしますが、複雑さが高くなります。まずCRUDで始め、必要に応じてイベントを追加しましょう。
Q R2DBCとJPA — どう選ぶ?
A 新規プロジェクト + WebFlux + コルーチン → R2DBC(ノンブロッキング)。既存のJPAプロジェクトやリアクティブに不慣れなチーム → JPA + コルーチン拡張。
Q マイクロサービスはどのくらい小さくすべき?
A 境界付けられたコンテキストごとに1つのマイクロサービス。OrderProcessorは「注文処理」コンテキストです — 決済や在庫を詰め込まないでください。
Q APIのバージョニングはどうする?
A URLパスバージョニング(
/api/v1/)が最もシンプルで直感的です。ヘッダーベースのバージョニングはよりRESTfulですが複雑です。URLバージョニングが推奨されます。Q アーキテクチャ設計が実装可能であることをどう確認する?
A すべてのアーキテクチャ決定にPOC(概念実証)コードを付けるべきです。紙上のアーキテクチャ + 実行可能なプロトタイプ = 実装可能な設計。
📖 まとめ
- 要件 → ドメインモデリング → ステートマシン → アーキテクチャ → 技術スタック → API → データベース
- レイヤードアーキテクチャ:Controller → Service → Repository → Integration
- sealed classで注文ステートマシンをモデリング、コンパイラによる網羅チェックを強制
- 技術スタック:Spring Boot + WebFlux + Coroutines + R2DBC
- RESTful API設計:リソース名詞 + HTTPメソッド + バージョニング + 統一エラーフォーマット
- イベントソーシング:状態変更イベントを保存し、監査とリプレイに対応
📝 練習問題
- 初級 (⭐):
sealed classを使ってシンプルなタスク管理システムのステートマシンを設計してください(Todo → InProgress → Done / Cancelled)。ヒント:sealed class TaskStatus - 中級 (⭐⭐):5つのエンドポイントに対するリクエスト/レスポンスdata classを含む、完全なRESTful APIコントラクトを設計してください。ヒント:
CreateOrderRequest/OrderResponse - 上級 (⭐⭐⭐):ECシステムの完全なレイヤードアーキテクチャドキュメントを設計してください。ドメインモデル、ステートマシン、API、データベーススキーマ、技術スタックを含む。ヒント:本課のすべてのセクションを参照