Kotlin: Desenvolvimento de Projetos Kotlin Explicado
Última atualização: 2026-08-26
O design está pronto — agora é hora de codar. Charlie, Alice e Bob colaboram para implementar o código de quatro camadas do OrderProcessor: camada de domínio (Order + eventos), camada de serviço (lógica de negócio + corrotinas), camada de persistência (Repository) e camada de integração (clientes HTTP).
1. O que Você Aprenderá
- Camada de domínio:
data class/sealed class/StateMachine - Camada de serviço:
OrderService+ processamento assíncrono + stream de eventosFlow - Camada de persistência: Spring Data R2DBC + Repository de corrotinas
- Camada de integração: Ktor HTTP Client para chamadas a serviços externos
- Fluxo de desenvolvimento colaborativo Alice/Bob/Charlie
2. A História Real de uma Equipe
(1) Problema: Divisão de Trabalho Incerta Causa Conflitos
Alice e Bob modificaram OrderService simultaneamente — 10 conflitos Git. Causa raiz: sem divisão de trabalho por camada, ambos editavam o mesmo arquivo.
(2) Solução de Divisão por Camada
Charlie: Camada de domínio (Order, OrderEvent, StateMachine)
Alice: Camada de serviço (OrderService, lógica de negócio)
Bob: Repository + Camada de integração (acesso a dados, clientes HTTP)
Arquitetura em camadas = divisão de trabalho em camadas. Cada camada se desenvolve independentemente, com contratos de interface minimizando conflitos.
3. Camada de Domínio
(1) Entidades Centrais
// Entidades de domínio - Kotlin puro, sem dependência de framework
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 e Eventos
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) Máquina de Estados
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. Camada de Serviço
(1) OrderService
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) DTOs de Requisição/Resposta
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. Camada de Persistência
(1) Interfaces de Repository
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) Implementação In-Memory (Fase de Desenvolvimento)
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. Camada de Integração
(1) Interfaces de Cliente HTTP
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()
}
// Implementações simuladas
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. Comparação de Responsabilidades das Quatro Camadas
| Camada | Responsabilidade | Direção de Dependência | Classes Típicas | Estratégia de Teste |
|---|---|---|---|---|
| Domínio | Regras de negócio e entidades | Sem dependências | Order, OrderStatus, StateMachine | Testes unitários puros |
| Serviço | Orquestração de processos de negócio | → Domínio/Persistência/Integração | OrderService | Testes unitários com dependências mockadas |
| Persistência | Acesso a dados | → Domínio | OrderRepository, EventRepository | Testes de integração (Testcontainers) |
| Integração | Chamadas a serviços externos | → Domínio | PaymentClient, InventoryClient | Testes de contrato + Mock |
(1) Comparação de Estratégias de Implementação do Repository
| Fase | Implementação | Prós | Contras |
|---|---|---|---|
| Desenvolvimento | InMemory* | Zero config, rápido | Sem persistência |
| Testes | Testcontainers | DB real, reprodutível | Requer Docker |
| Produção | Spring Data R2DBC | Nativo para corrotinas, alta performance | Config de pool de conexões necessária |
(2) Tabela de Regras de Transição de Estado
| Estado Atual | Evento Permitido | Estado Destino |
|---|---|---|
| Pending | Confirmed | Confirmed |
| Pending | Cancelled | Cancelled |
| Confirmed | Shipped | Shipped |
| Shipped | Delivered | Delivered |
| Outras combinações | Ilegais | — |
8. Sequência Completa de Criação de Pedido
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: Disponível
Service->>Repo: save(order)
Repo-->>Service: Salvo
Service->>Events: append(Created event)
Service->>Payment: processPayment(orderId, total)
Payment-->>Service: Sucesso
Service->>Repo: save(order status=Confirmed)
Service->>Events: append(Confirmed event)
Service-->>Controller: Order
Controller-->>Client: 201 Created
9. Exemplo Completo
// ============================================
// OrderProcessor - Implementação End-to-End
// Funcionalidade: Ciclo de vida completo do pedido com máquina de estados
// ============================================
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
// --- Domínio (das seções acima) ---
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 }
}
// --- Serviço ---
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("=== Demo de Desenvolvimento do OrderProcessor ===\n")
// Criar pedidos
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("Criado: ${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("Criado: ${o2.id} | Total: \$${o2.total} USD | Status: ${o2.status}")
// Confirmar
val confirmed = service.confirm(o1.id, "2026-07-13T10:05:00Z")
println("\nConfirmado: ${confirmed.id} | Status: ${confirmed.status}")
// Enviar
val shipped = service.ship(o1.id, "TRK-ABC123", "2026-07-13T11:00:00Z")
println("Enviado: ${shipped.id} | Rastreio: ${shipped.trackingCode} | Status: ${shipped.status}")
// Cancelar
val cancelled = service.cancel(o2.id, "Solicitação do cliente", "2026-07-13T10:10:00Z")
println("Cancelado: ${cancelled.id} | Motivo: ${(cancelled.status as OrderStatus.Cancelled).reason}")
// Histórico de eventos
println("\n=== Histórico de Eventos: ${o1.id} ===")
service.history(o1.id).forEach { println(" $it") }
}
Saída:
=== Demo de Desenvolvimento do OrderProcessor ===
Criado: ORD-1 | Total: $179.96 USD | Status: Pending(at=2026-07-13T10:00:00Z)
Criado: ORD-2 | Total: $10000.0 USD | Status: Pending(at=2026-07-13T10:01:00Z)
Confirmado: ORD-1 | Status: Confirmed(at=2026-07-13T10:05:00Z)
Enviado: ORD-1 | Rastreio: TRK-ABC123 | Status: Shipped(trackingCode=TRK-ABC123, at=2026-07-13T11:00:00Z)
Cancelado: ORD-2 | Motivo: Solicitação do cliente
=== Histórico de Eventos: 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)
9. Exemplos práticos rápidos
▶ Exemplo: Git workflow com feature branches
# Inicializar projeto
git init
git checkout -b main
git add .
git commit -m "feat: initial project structure"
# Feature branch
git checkout -b feature/order-creation
# ... desenvolver, commitar ...
git add .
git commit -m "feat(order): add CreateOrder use case"
# Push e abrir PR
git push origin feature/order-creation
# Abrir PR no GitHub, solicitar review
# Após aprovação, merge para main
# Manutenção: hotfix branch
git checkout main
git checkout -b hotfix/critical-bug
git commit -m "fix: race condition in order creation"
▶ Exemplo: Commits semânticos
# Formato: tipo(escopo): descrição
feat: adiciona novo endpoint de cancelamento
feat(order): suporte para pedidos recorrentes
fix: corrige cálculo de imposto para pedidos internacionais
fix(cart): evita duplicação de itens
docs: atualiza README com novos exemplos
docs(api): adiciona exemplos de uso de REST
style: reformata código conforme ktlint
refactor: extrai validação para OrderValidator class
test: adiciona testes para ConfirmOrderUseCase
test(integration): testa fluxo completo de pedido
chore: atualiza dependências do Gradle
build: configura profile dev e prod
ci: adiciona workflow para GitHub Actions
perf: cacheia lookups de OrderRepository
▶ Exemplo: Pull request template
<!-- .github/pull_request_template.md -->
## Descrição
<!-- Resumo das mudanças -->
## Motivação e Contexto
<!-- Por que essas mudanças são necessárias? -->
## Como foi testado?
<!-- Descreva testes que você executou -->
## Tipo de Mudança
- [ ] Bug fix (non-breaking)
- [ ] Nova feature (non-breaking)
- [ ] Breaking change
- [ ] Documentação
## Checklist
- [ ] Código segue estilo do projeto
- [ ] Revisei minha próprias mudanças
- [ ] Comentários adicionados onde necessário
- [ ] Documentação atualizada
- [ ] Sem novos warnings
- [ ] Testes adicionados/atualizados
- [ ] Todos os testes passam localmente
## Capturas de tela (se aplicável)
## Notas adicionais
▶ Exemplo: Code review checklist
// Ao revisar PR, verificar:
// [ ] 1. Design: está alinhado com arquitetura?
// [ ] 2. Funcionalidade: comportamento correto?
// [ ] 3. Complexidade: pode ser simplificado?
// [ ] 4. Tests: cobertura adequada?
// [ ] 5. Naming: nomes expressivos?
// [ ] 6. Error handling: erros tratados adequadamente?
// [ ] 7. Performance: há bottlenecks?
// [ ] 8. Security: vetores de ataque cobertos?
// [ ] 9. Documentation: atualizado?
// [ ] 10. Style: segue convenções do projeto?
// Exemplo de feedback construtivo:
// ❌ "Este código está ruim"
// ✅ "Considere extrair esta validação para uma função separada
// para melhorar legibilidade e testabilidade"
▶ Exemplo: CI/CD GitHub Actions
# .github/workflows/build.yml
name: Build and Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
- name: Build with Gradle
run: ./gradlew build --no-daemon
- name: Run tests
run: ./gradlew test --no-daemon
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./build/reports/jacoco/test/jacocoTestReport.xml
▶ Exemplo: Versionamento semântico
// versionamento_semantico.kts
// Formato: MAJOR.MINOR.PATCH
// MAJOR: mudanças incompatíveis em API
// MINOR: novas features backward-compatíveis
// PATCH: bug fixes backward-compatíveis
// git tags
// git tag -a v1.0.0 -m "Primeira release estável"
// git tag -a v1.1.0 -m "Adiciona pedidos recorrentes"
// git tag -a v1.1.1 -m "Corrige cálculo de frete"
// git tag -a v2.0.0 -m "Breaking change: nova estrutura de DTOs"
// Em build.gradle.kts:
version = "1.1.0"
group = "com.example"
// Gerar changelog automático
// git log v1.0.0..v1.1.0 --oneline
▶ Exemplo: Documentação com KDoc
/**
* Processador de pedidos - gerencia o ciclo de vida.
*
* Use cases cobertos:
* 1. Criação: validate -> persist -> notify
* 2. Confirmação: lookup -> state-transition -> notify
* 3. Cancelamento: lookup -> state-transition -> notify
*
* @property repository Repositório de pedidos
* @property notifier Serviço de notificações
*/
class OrderProcessor(
private val repository: OrderRepository,
private val notifier: Notifier
) {
/**
* Confirma um pedido pendente.
*
* @param orderId ID do pedido
* @return Result com pedido confirmado ou erro
* @throws IllegalStateException se pedido não está no estado correto
*/
suspend fun confirm(orderId: String): Result<Order> {
val order = repository.findById(orderId)
?: return Result.failure(NoSuchElementException("Pedido $orderId não existe"))
check(order.status == OrderStatus.Created) {
"Pedido não pode ser confirmado - estado atual: ${order.status}"
}
val confirmed = order.copy(status = OrderStatus.Confirmed(Instant.now()))
val saved = repository.save(confirmed)
notifier.notify(saved)
return Result.success(saved)
}
}
Saída:
KDoc gera documentação HTML/JavaDoc automática
em sites como javadoc.io ou Dokka
❓ Perguntas Frequentes
P: A camada de domínio deve depender de um framework? R: Não. A camada de domínio é código Kotlin puro sem dependência do Spring ou qualquer framework. Isso garante que a lógica de domínio pode ser testada e reutilizada independentemente.
P: A camada de Serviço deve retornar objetos de domínio ou DTOs? R: O Serviço retorna objetos de domínio; o Controller os converte para DTOs. Responsabilidade clara de camada: o Serviço não tem consciência do HTTP.
P: Onde a máquina de estados deve ficar? R: Na camada de domínio. A máquina de estados é lógica de negócio central e não deve depender de nenhuma camada externa. Implemente-a com sealed classes puras do Kotlin.
P: Como lidar com atualizações concorrentes de estado? R: Locking otimista no banco de dados (campo version) ou locks distribuídos. No nível de corrotinas, use
Mutexpara proteger estado compartilhado. Para produção, locking no banco de dados é recomendado.
P: Qual cliente HTTP a camada de integração deve usar? R: Para projetos Kotlin, Ktor Client (nativo para corrotinas) é recomendado. Em ecossistemas Java, Spring WebClient também funciona. Ambos suportam
suspend.
P: Como garantir que a equipe divida o trabalho por camada? R: Regras de code review: Controllers não tocam diretamente o Repository, Services não sabem sobre HTTP, a camada de Domínio não depende de frameworks. Ferramentas como ArchUnit podem garantir isso automaticamente.
📖 Resumo
- Camada de domínio: Kotlin puro, data class + sealed class + máquina de estados, sem dependência de framework
- Camada de serviço: lógica de negócio + transições de estado + publicação de eventos, chamando Repository e camada de Integração
- Camada de persistência: abstração de interface + implementação in-memory (dev) + implementação de banco de dados (prod)
- Camada de integração: interfaces de cliente HTTP + implementações Mock (dev) + implementações Ktor/Spring (prod)
- Divisão de trabalho por camada: Charlie cuida do domínio, Alice cuida do serviço, Bob cuida da persistência/integração
- Máquina de estados + event sourcing: registra eventos para cada mudança de estado, suportando trilha de auditoria completa
📝 Exercícios
- Iniciante (⭐): Implemente a data class
OrderIteme a sealed classOrderStatuscom validação no blocoinit. Dica:require() - Intermediário (⭐⭐): Implemente os métodos
confirm()eship()doOrderServiceusando a máquina de estados para validar transições. Dica:StateMachine.transition() - Avançado (⭐⭐⭐): Implemente o OrderProcessor completo de quatro camadas: domínio (Order + Event + StateMachine), serviço (OrderService + corrotinas), persistência (InMemoryRepo), integração (MockPaymentClient). Dica: Consulte o exemplo completo da seção 9