404 Not Found

404 Not Found


nginx

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


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

100%
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


📝 Exercises

  1. Beginner (⭐): Implement the OrderItem data class and OrderStatus sealed class with validation in the init block. Hint: require()
  2. Intermediate (⭐⭐): Implement OrderService's confirm() and ship() methods using the state machine to validate transitions. Hint: StateMachine.transition()
  3. 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

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏