Kotlin Project Development Explained
Design is done — now it's code time. Charlie, Alice, and Bob collaborate to implement the OrderProcessor's four-layer code: domain layer (Order + events), service layer (business logic + coroutines), persistence layer (Repository), and integration layer (HTTP clients).
1. What You'll Learn
- Domain layer:
data class/sealed class/StateMachine - Service layer:
OrderService+ async processing +Flowevent stream - Persistence layer: Spring Data R2DBC + coroutine Repository
- Integration layer: Ktor HTTP Client for external service calls
- Alice/Bob/Charlie collaborative development workflow
2. A Team's Real Story
(1) Pain Point: Unclear Division of Labor Causes Conflicts
Alice and Bob modified OrderService simultaneously — 10 Git conflicts. Root cause: no layer-based division of labor meant both were editing the same file.
(2) Layer-Based Division Solution
TEXT
Charlie: Domain layer (Order, OrderEvent, StateMachine)
Alice: Service layer (OrderService, business logic)
Bob: Repository + Integration layer (data access, HTTP clients)
Layered architecture = layered work division. Each layer develops independently, with interface contracts minimizing conflicts.
3. Domain Layer
(1) Core Entities
KOTLIN
// Domain entities - pure Kotlin, no framework dependency
data class OrderItem(
val sku: String,
val quantity: Int,
val unitPrice: Double
) {
val subtotal: Double get() = quantity * unitPrice
init {
require(quantity > 0) { "Quantity must be positive" }
require(unitPrice >= 0) { "Price must be non-negative" }
}
}
data class Address(
val street: String,
val city: String,
val country: String
)
data class Order(
val id: String,
val customerId: String,
val items: List<OrderItem>,
val shippingAddress: Address?,
var status: OrderStatus,
var trackingCode: String?,
val createdAt: String,
var updatedAt: String?
) {
val total: Double get() = items.sumOf { it.subtotal }
init {
require(id.startsWith("ORD-")) { "Invalid order ID format" }
require(items.isNotEmpty()) { "Order must have at least one item" }
}
}
(2) Status and Events
KOTLIN
sealed class OrderStatus {
data class Pending(val createdAt: String) : OrderStatus()
data class Confirmed(val confirmedAt: String) : OrderStatus()
data class Shipped(val trackingCode: String, val shippedAt: String) : OrderStatus()
data class Delivered(val deliveredAt: String) : OrderStatus()
data class Cancelled(val reason: String, val cancelledAt: String) : OrderStatus()
}
sealed class OrderEvent {
abstract val orderId: String
abstract val timestamp: String
data class Created(
override val orderId: String,
val customerId: String,
val total: Double,
override val timestamp: String
) : OrderEvent()
data class Confirmed(override val orderId: String, override val timestamp: String) : OrderEvent()
data class Shipped(override val orderId: String, val trackingCode: String, override val timestamp: String) : OrderEvent()
data class Delivered(override val orderId: String, override val timestamp: String) : OrderEvent()
data class Cancelled(override val orderId: String, val reason: String, override val timestamp: String) : OrderEvent()
}
(3) State Machine
KOTLIN
object OrderStateMachine {
fun transition(current: OrderStatus, event: OrderEvent): OrderStatus = when {
current is OrderStatus.Pending && event is OrderEvent.Confirmed ->
OrderStatus.Confirmed(event.timestamp)
current is OrderStatus.Confirmed && event is OrderEvent.Shipped ->
OrderStatus.Shipped(event.trackingCode, event.timestamp)
current is OrderStatus.Shipped && event is OrderEvent.Delivered ->
OrderStatus.Delivered(event.timestamp)
current is OrderStatus.Pending && event is OrderEvent.Cancelled ->
OrderStatus.Cancelled(event.reason, event.timestamp)
else ->
throw IllegalStateException("Invalid transition from $current with $event")
}
fun canTransition(current: OrderStatus, event: OrderEvent): Boolean = when {
current is OrderStatus.Pending && event is OrderEvent.Confirmed -> true
current is OrderStatus.Confirmed && event is OrderEvent.Shipped -> true
current is OrderStatus.Shipped && event is OrderEvent.Delivered -> true
current is OrderStatus.Pending && event is OrderEvent.Cancelled -> true
else -> false
}
}
4. Service Layer
(1) OrderService
KOTLIN
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class OrderService(
private val repository: OrderRepository,
private val eventStore: EventRepository,
private val paymentClient: PaymentClient,
private val inventoryClient: InventoryClient
) {
suspend fun createOrder(request: CreateOrderRequest): Order {
val order = Order(
id = generateOrderId(),
customerId = request.customerId,
items = request.items.map { OrderItem(it.sku, it.quantity, it.unitPrice) },
shippingAddress = request.shippingAddress,
status = OrderStatus.Pending(request.timestamp),
trackingCode = null,
createdAt = request.timestamp,
updatedAt = null
)
repository.save(order)
eventStore.append(OrderEvent.Created(order.id, order.customerId, order.total, request.timestamp))
return order
}
suspend fun confirmOrder(orderId: String, timestamp: String): Order {
val order = repository.findById(orderId) ?: throw NoSuchElementException("Order $orderId not found")
val event = OrderEvent.Confirmed(orderId, timestamp)
val newStatus = OrderStateMachine.transition(order.status, event)
order.status = newStatus
order.updatedAt = timestamp
repository.save(order)
eventStore.append(event)
return order
}
suspend fun shipOrder(orderId: String, trackingCode: String, timestamp: String): Order {
val order = repository.findById(orderId) ?: throw NoSuchElementException("Order $orderId not found")
val event = OrderEvent.Shipped(orderId, trackingCode, timestamp)
val newStatus = OrderStateMachine.transition(order.status, event)
order.status = newStatus
order.trackingCode = trackingCode
order.updatedAt = timestamp
repository.save(order)
eventStore.append(event)
return order
}
fun orderEvents(orderId: String): Flow<OrderEvent> = flow {
eventStore.findByOrderId(orderId).forEach { emit(it) }
}
private fun generateOrderId(): String = "ORD-${System.currentTimeMillis()}"
}
(2) Request/Response DTOs
KOTLIN
data class CreateOrderRequest(
val customerId: String,
val items: List<CreateItemRequest>,
val shippingAddress: AddressRequest?,
val timestamp: String
)
data class CreateItemRequest(val sku: String, val quantity: Int, val unitPrice: Double)
data class AddressRequest(val street: String, val city: String, val country: String)
data class OrderResponse(
val id: String,
val customerId: String,
val total: Double,
val status: String,
val items: List<OrderItemResponse>,
val trackingCode: String?,
val createdAt: String,
val updatedAt: String?
)
data class OrderItemResponse(val sku: String, val quantity: Int, val unitPrice: Double, val subtotal: Double)
fun Order.toResponse() = OrderResponse(
id, customerId, total, status::class.simpleName ?: "Unknown",
items.map { OrderItemResponse(it.sku, it.quantity, it.unitPrice, it.subtotal) },
trackingCode, createdAt, updatedAt
)
5. Persistence Layer
(1) Repository Interfaces
KOTLIN
interface OrderRepository {
suspend fun save(order: Order)
suspend fun findById(id: String): Order?
suspend fun findAll(): List<Order>
suspend fun deleteById(id: String)
}
interface EventRepository {
suspend fun append(event: OrderEvent)
fun findByOrderId(orderId: String): List<OrderEvent>
}
(2) In-Memory Implementation (Development Phase)
KOTLIN
class InMemoryOrderRepository : OrderRepository {
private val storage = mutableMapOf<String, Order>()
override suspend fun save(order: Order) { storage[order.id] = order }
override suspend fun findById(id: String) = storage[id]
override suspend fun findAll() = storage.values.toList()
override suspend fun deleteById(id: String) { storage.remove(id) }
}
class InMemoryEventRepository : EventRepository {
private val events = mutableListOf<OrderEvent>()
override suspend fun append(event: OrderEvent) { events.add(event) }
override fun findByOrderId(orderId: String) = events.filter { it.orderId == orderId }
}
6. Integration Layer
(1) HTTP Client Interfaces
KOTLIN
interface PaymentClient {
suspend fun processPayment(orderId: String, amount: Double): PaymentResult
}
interface InventoryClient {
suspend fun checkAvailability(sku: String, quantity: Int): Boolean
suspend fun reserve(sku: String, quantity: Int): ReservationResult
}
sealed class PaymentResult {
data class Success(val transactionId: String) : PaymentResult()
data class Failed(val reason: String) : PaymentResult()
}
sealed class ReservationResult {
data class Reserved(val reservationId: String) : ReservationResult()
data class Unavailable(val reason: String) : ReservationResult()
}
// Simulated implementations
class MockPaymentClient : PaymentClient {
override suspend fun processPayment(orderId: String, amount: Double) =
PaymentResult.Success("TXN-${orderId.substring(4)}")
}
class MockInventoryClient : InventoryClient {
override suspend fun checkAvailability(sku: String, quantity: Int) = quantity < 10_000
override suspend fun reserve(sku: String, quantity: Int) =
ReservationResult.Reserved("RES-${sku.substring(4)}")
}
7. Four-Layer Responsibility Comparison
| Layer | Responsibility | Dependency Direction | Typical Classes | Testing Strategy |
|---|---|---|---|---|
| Domain | Business rules & entities | No dependencies | Order, OrderStatus, StateMachine | Pure unit tests |
| Service | Business process orchestration | → Domain/Persistence/Integration | OrderService | Unit tests with mocked dependencies |
| Persistence | Data access | → Domain | OrderRepository, EventRepository | Integration tests (Testcontainers) |
| Integration | External service calls | → Domain | PaymentClient, InventoryClient | Contract tests + Mock |
(1) Repository Implementation Strategy Comparison
| Phase | Implementation | Pros | Cons |
|---|---|---|---|
| Development | InMemory* | Zero config, fast | No persistence |
| Testing | Testcontainers | Real DB, reproducible | Requires Docker |
| Production | Spring Data R2DBC | Coroutine-native, high performance | Connection pool config needed |
(2) State Transition Rules Table
| Current State | Allowed Event | Target State |
|---|---|---|
| Pending | Confirmed | Confirmed |
| Pending | Cancelled | Cancelled |
| Confirmed | Shipped | Shipped |
| Shipped | Delivered | Delivered |
| Other combos | Illegal | — |
8. Complete Order Creation Sequence
sequenceDiagram
participant Client
participant Controller
participant Service
participant Repo
participant Events
participant Payment
participant Inventory
Client->>Controller: POST /api/v1/orders
Controller->>Service: createOrder(request)
Service->>Inventory: checkAvailability(sku, qty)
Inventory-->>Service: Available
Service->>Repo: save(order)
Repo-->>Service: Saved
Service->>Events: append(Created event)
Service->>Payment: processPayment(orderId, total)
Payment-->>Service: Success
Service->>Repo: save(order status=Confirmed)
Service->>Events: append(Confirmed event)
Service-->>Controller: Order
Controller-->>Client: 201 Created
9. Complete Example
KOTLIN
// ============================================
// OrderProcessor - End-to-End Implementation
// Feature: Full order lifecycle with state machine
// ============================================
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
// --- Domain (from sections above) ---
data class OrderItem(val sku: String, val quantity: Int, val unitPrice: Double) {
val subtotal: Double get() = quantity * unitPrice
}
sealed class OrderStatus {
data class Pending(val at: String) : OrderStatus()
data class Confirmed(val at: String) : OrderStatus()
data class Shipped(val trackingCode: String, val at: String) : OrderStatus()
data class Cancelled(val reason: String, val at: String) : OrderStatus()
}
sealed class OrderEvent {
abstract val orderId: String
data class Created(override val orderId: String, val total: Double, val at: String) : OrderEvent()
data class Confirmed(override val orderId: String, val at: String) : OrderEvent()
data class Shipped(override val orderId: String, val code: String, val at: String) : OrderEvent()
data class Cancelled(override val orderId: String, val reason: String, val at: String) : OrderEvent()
}
data class Order(
val id: String, val customerId: String, val items: List<OrderItem>,
var status: OrderStatus, var trackingCode: String?, val createdAt: String, var updatedAt: String?
) { val total: Double get() = items.sumOf { it.subtotal } }
object StateMachine {
fun next(current: OrderStatus, event: OrderEvent): OrderStatus = when {
current is OrderStatus.Pending && event is OrderEvent.Confirmed -> OrderStatus.Confirmed(event.at)
current is OrderStatus.Confirmed && event is OrderEvent.Shipped -> OrderStatus.Shipped(event.code, event.at)
current is OrderStatus.Pending && event is OrderEvent.Cancelled -> OrderStatus.Cancelled(event.reason, event.at)
else -> error("Invalid: $current + $event")
}
}
// --- Repository ---
class OrderRepo {
private val db = mutableMapOf<String, Order>()
fun save(o: Order) { db[o.id] = o }
fun find(id: String) = db[id]
fun findAll() = db.values.toList()
}
class EventRepo {
private val events = mutableListOf<OrderEvent>()
fun append(e: OrderEvent) { events.add(e) }
fun find(orderId: String) = events.filter { it.orderId == orderId }
}
// --- Service ---
class OrderService(private val orders: OrderRepo, private val events: EventRepo) {
private var counter = 0
fun create(customerId: String, items: List<OrderItem>, at: String): Order {
val order = Order("ORD-${++counter}", customerId, items, OrderStatus.Pending(at), null, at, null)
orders.save(order)
events.append(OrderEvent.Created(order.id, order.total, at))
return order
}
fun confirm(id: String, at: String): Order {
val order = orders.find(id) ?: error("Not found: $id")
val event = OrderEvent.Confirmed(id, at)
order.status = StateMachine.next(order.status, event)
order.updatedAt = at
orders.save(order)
events.append(event)
return order
}
fun ship(id: String, code: String, at: String): Order {
val order = orders.find(id) ?: error("Not found: $id")
val event = OrderEvent.Shipped(id, code, at)
order.status = StateMachine.next(order.status, event)
order.trackingCode = code
order.updatedAt = at
orders.save(order)
events.append(event)
return order
}
fun cancel(id: String, reason: String, at: String): Order {
val order = orders.find(id) ?: error("Not found: $id")
val event = OrderEvent.Cancelled(id, reason, at)
order.status = StateMachine.next(order.status, event)
order.updatedAt = at
orders.save(order)
events.append(event)
return order
}
fun history(id: String) = events.find(id)
}
// --- Demo ---
fun main() {
val service = OrderService(OrderRepo(), EventRepo())
println("=== OrderProcessor Development Demo ===\n")
// Create orders
val o1 = service.create("CUST-001", listOf(OrderItem("SKU-A", 3, 9.99), OrderItem("SKU-B", 1, 149.99)), "2026-07-13T10:00:00Z")
println("Created: ${o1.id} | Total: \$${o1.total} USD | Status: ${o1.status}")
val o2 = service.create("CUST-002", listOf(OrderItem("SKU-C", 2, 5_000.0)), "2026-07-13T10:01:00Z")
println("Created: ${o2.id} | Total: \$${o2.total} USD | Status: ${o2.status}")
// Confirm
val confirmed = service.confirm(o1.id, "2026-07-13T10:05:00Z")
println("\nConfirmed: ${confirmed.id} | Status: ${confirmed.status}")
// Ship
val shipped = service.ship(o1.id, "TRK-ABC123", "2026-07-13T11:00:00Z")
println("Shipped: ${shipped.id} | Tracking: ${shipped.trackingCode} | Status: ${shipped.status}")
// Cancel
val cancelled = service.cancel(o2.id, "Customer request", "2026-07-13T10:10:00Z")
println("Cancelled: ${cancelled.id} | Reason: ${(cancelled.status as OrderStatus.Cancelled).reason}")
// Event history
println("\n=== Event History: ${o1.id} ===")
service.history(o1.id).forEach { println(" $it") }
}
Output:
TEXT
=== OrderProcessor Development Demo ===
Created: ORD-1 | Total: $179.96 USD | Status: Pending(at=2026-07-13T10:00:00Z)
Created: ORD-2 | Total: $10000.0 USD | Status: Pending(at=2026-07-13T10:01:00Z)
Confirmed: ORD-1 | Status: Confirmed(at=2026-07-13T10:05:00Z)
Shipped: ORD-1 | Tracking: TRK-ABC123 | Status: Shipped(trackingCode=TRK-ABC123, at=2026-07-13T11:00:00Z)
Cancelled: ORD-2 | Reason: Customer request
=== Event History: ORD-1 ===
Created(orderId=ORD-1, total=179.96, at=2026-07-13T10:00:00Z)
Confirmed(orderId=ORD-1, at=2026-07-13T10:05:00Z)
Shipped(orderId=ORD-1, code=TRK-ABC123, at=2026-07-13T11:00:00Z)
❓ FAQ
Q Should the domain layer depend on a framework?
A No. The domain layer is pure Kotlin code with no Spring or framework dependency. This ensures domain logic can be independently tested and reused.
Q Should the Service layer return domain objects or DTOs?
A Service returns domain objects; the Controller converts them to DTOs. Clear layer responsibility: the Service has no awareness of HTTP.
Q Where should the state machine live?
A In the domain layer. The state machine is core business logic and should not depend on any external layer. Implement it with pure Kotlin sealed classes.
Q How to handle concurrent state updates?
A Database optimistic locking (version field) or distributed locks. At the coroutine level, use
Mutex to protect shared state. For production, database locking is recommended.Q Which HTTP client should the integration layer use?
A For Kotlin projects, Ktor Client (coroutine-native) is recommended. In Java ecosystems, Spring WebClient works too. Both support
suspend.Q How to ensure the team divides work by layer?
A Code review rules: Controllers don't directly touch Repository, Services don't know about HTTP, the Domain layer doesn't depend on frameworks. Tools like ArchUnit can enforce this automatically.
📖 Summary
- Domain layer: pure Kotlin, data class + sealed class + state machine, no framework dependency
- Service layer: business logic + state transitions + event publishing, calling Repository and Integration layer
- Persistence layer: interface abstraction + in-memory implementation (dev) + database implementation (prod)
- Integration layer: HTTP client interfaces + Mock implementations (dev) + Ktor/Spring implementations (prod)
- Layered work division: Charlie handles domain, Alice handles service, Bob handles persistence/integration
- State machine + event sourcing: record events for every state change, supporting a complete audit trail
📝 Exercises
- Beginner (⭐): Implement the
OrderItemdata class andOrderStatussealed class with validation in theinitblock. Hint:require() - Intermediate (⭐⭐): Implement
OrderService'sconfirm()andship()methods using the state machine to validate transitions. Hint:StateMachine.transition() - Advanced (⭐⭐⭐): Implement the complete four-layer OrderProcessor: domain (Order + Event + StateMachine), service (OrderService + coroutines), persistence (InMemoryRepo), integration (MockPaymentClient). Hint: Reference section 9's complete example



