Kotlin Project Design Explained
All 24 lessons converge here — Charlie starts from requirements analysis and designs the OrderProcessor's domain model, layered architecture, tech stack selection, and API contracts. This is step one of the real-world project.
1. What You'll Learn
- Requirements analysis and domain modeling: order state machine, event sourcing
- Layered architecture: Controller → Service → Repository → Domain
- Tech stack: Spring Boot + Coroutines + kotlinx.serialization + Ktor Client
- API design: RESTful endpoint planning + OpenAPI documentation
- Charlie's hands-on: OrderProcessor architecture diagram and database schema
2. An Architect's Real Story
(1) Pain Point: Designing Without a Plan Guarantees Rework
Charlie's team once skipped design and started coding directly. Three months later, the domain model didn't match business requirements — 60% of the code had to be rewritten. Loss: 3 months of development + 2 months of rework.
(2) Design-First Approach
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)
Invest 3 weeks in design, reduce rework by 60%. "Fast development" without design is the slowest path.
3. Requirements Analysis and Domain Modeling
(1) Core Requirements
| Requirement | Description | Priority |
|---|---|---|
| Order creation | Customer places order, generates pending order | P0 |
| Order payment | Confirm order after successful payment | P0 |
| Order shipping | Warehouse ships, generates tracking number | P0 |
| Order cancellation | Customer/system cancels order, triggers refund | P0 |
| High-value routing | Orders > 10,000 USD go through VIP pipeline | P1 |
| Batch processing | Support 5,000 orders/sec throughput | P1 |
| Event sourcing | Order status changes produce domain events | P2 |
(2) Domain Model
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) Order State Machine
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 --> [*]
4. Layered Architecture
(1) Four-Layer Architecture
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) Layer Responsibilities
| Layer | Responsibility | Key Technology |
|---|---|---|
| Controller | HTTP request handling, validation, response | Spring WebFlux, suspend |
| Service | Business logic, state machine, event publishing | Coroutines, sealed class |
| Repository | Data persistence, queries | R2DBC, JPA |
| Integration | External service calls | Ktor Client, Resilience4j |
(3) Project Module Structure
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
5. Tech Stack Selection
| Domain | Technology Choice | Reason |
|---|---|---|
| Framework | Spring Boot 3.2 + WebFlux | Mature ecosystem, coroutine support |
| Language | Kotlin 1.9 | Null safety, coroutines, data class |
| Async | kotlinx.coroutines | Structured concurrency, suspend |
| Serialization | kotlinx.serialization | Compile-time safety, multi-format |
| Database | PostgreSQL + R2DBC | Reactive driver, coroutine-friendly |
| HTTP Client | Ktor Client | Kotlin-native, coroutine support |
| Cache | Redis + kotlinx.coroutines | Async caching |
| Testing | JUnit 5 + MockK | Kotlin-native mocking |
| Monitoring | Micrometer + Prometheus | Metrics collection |
6. API Design
(1) RESTful Endpoints
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) Request/Response DTOs
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 Design Principles
| Principle | Practice |
|---|---|
| RESTful | Resource nouns + HTTP method semantics |
| Versioning | /api/v1/ prefix |
| Pagination | ?page=0&size=20 |
| Filtering | ?status=CONFIRMED&customer=C001 |
| HATEOAS | Include related links in response (optional) |
| Error format | Unified ErrorResponse |
7. Database Schema
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. Complete Example: OrderProcessor Architecture Design Document
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")
}
Output:
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
❓ FAQ
Q Design the database first or the API first?
A Design the domain model first (DDD approach). Then both the API and database are projections of the domain model. The domain model is core; the API and Schema are peripherals.
Q What's the difference between event sourcing and CRUD?
A CRUD only stores current state. Event sourcing stores all state-change events. Event sourcing supports auditing, replay, and time-travel, but has higher complexity. Start with CRUD; add events when needed.
Q R2DBC or JPA — how to choose?
A New project + WebFlux + coroutines → R2DBC (non-blocking). Existing JPA project or team unfamiliar with reactive → JPA + coroutine extensions.
Q How small should a microservice be?
A One microservice per bounded context. OrderProcessor is the "order processing" context — don't stuff payment and inventory into it too.
Q How to handle API versioning?
A URL path versioning (
/api/v1/) is simplest and most intuitive. Header-based versioning is more RESTful but more complex. URL versioning is recommended.Q How to ensure architecture design is implementable?
A Every architectural decision should have a POC (proof of concept) code. Architecture on paper + runnable prototype = implementable design.
📖 Summary
- Requirements → Domain modeling → State machine → Architecture → Tech stack → API → Database
- Layered architecture: Controller → Service → Repository → Integration
- Sealed classes model the order state machine with compiler-enforced exhaustive checks
- Tech stack: Spring Boot + WebFlux + Coroutines + R2DBC
- RESTful API design: resource nouns + HTTP methods + versioning + unified error format
- Event sourcing: store state-change events for auditing and replay
📝 Exercises
- Beginner (⭐): Use
sealed classto design a state machine for a simple task management system (Todo → InProgress → Done / Cancelled). Hint:sealed class TaskStatus - Intermediate (⭐⭐): Design a complete RESTful API contract with request/response data classes for 5 endpoints. Hint:
CreateOrderRequest/OrderResponse - Advanced (⭐⭐⭐): Design a complete layered architecture document for an e-commerce system, including domain model, state machine, API, database schema, and tech stack. Hint: Reference all sections of this lesson



