Kotlin: Kotlinプロジェクト設計詳解

最終更新:2026-08-26

これまでの24課がここに集約されます。Charlieは要件分析から始め、OrderProcessorのドメインモデル、レイヤードアーキテクチャ、技術スタック選定、APIコントラクトを設計します。これが実プロジェクトの第一歩です。

1. 学べること


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) 注文ステートマシン

100%
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層アーキテクチャ

100%
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(概念実証)コードを付けるべきです。紙上のアーキテクチャ + 実行可能なプロトタイプ = 実装可能な設計。

📖 まとめ


📝 練習問題

  1. 初級 (⭐)sealed classを使ってシンプルなタスク管理システムのステートマシンを設計してください(Todo → InProgress → Done / Cancelled)。ヒント:sealed class TaskStatus
  2. 中級 (⭐⭐):5つのエンドポイントに対するリクエスト/レスポンスdata classを含む、完全なRESTful APIコントラクトを設計してください。ヒント:CreateOrderRequest / OrderResponse
  3. 上級 (⭐⭐⭐):ECシステムの完全なレイヤードアーキテクチャドキュメントを設計してください。ドメインモデル、ステートマシン、API、データベーススキーマ、技術スタックを含む。ヒント:本課のすべてのセクションを参照

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%