Kotlin: Design de Projetos Kotlin Explicado
Última atualização: 2026-08-26
Todas as 24 lições convergem aqui — Charlie parte da análise de requisitos e desenha o modelo de domínio, arquitetura em camadas, seleção de stack tecnológico e contratos de API do OrderProcessor. Este é o passo um do projeto no mundo real.
1. O que Você Aprenderá
- Análise de requisitos e modelagem de domínio: máquina de estados de pedidos, event sourcing
- Arquitetura em camadas: Controller → Service → Repository → Domain
- Stack tecnológico: Spring Boot + Coroutines + kotlinx.serialization + Ktor Client
- Design de API: planejamento de endpoints RESTful + documentação OpenAPI
- Charlie na prática: diagrama de arquitetura do OrderProcessor e esquema de banco de dados
2. A História Real de um Arquiteto
(1) Problema: Projetar Sem Planejamento Garante Retrabalho
A equipe do Charlie uma vez pulou o design e começou a codificar diretamente. Três meses depois, o modelo de domínio não correspondia aos requisitos de negócio — 60% do código teve que ser reescrito. Perda: 3 meses de desenvolvimento + 2 meses de retrabalho.
(2) Abordagem Design-First
Semana 1: Requisitos → Modelo de Domínio → Máquina de Estados
Semana 2: Arquitetura → Stack Tecnológico → Contrato de API
Semana 3: Esquema de Banco de Dados → Fronteiras de Módulos → Plano CI/CD
Semana 4+: Desenvolvimento (com blueprint claro)
Invista 3 semanas em design, reduza retrabalho em 60%. "Desenvolvimento rápido" sem design é o caminho mais lento.
3. Análise de Requisitos e Modelagem de Domínio
(1) Requisitos Centrais
| Requisito | Descrição | Prioridade |
|---|---|---|
| Criação de pedido | Cliente faz pedido, gera pedido pendente | P0 |
| Pagamento do pedido | Confirmar pedido após pagamento bem-sucedido | P0 |
| Envio do pedido | Armazém envia, gera número de rastreio | P0 |
| Cancelamento do pedido | Cliente/sistema cancela pedido, aciona reembolso | P0 |
| Roteamento de alto valor | Pedidos > 10.000 USD passam por pipeline VIP | P1 |
| Processamento em lote | Suportar throughput de 5.000 pedidos/seg | P1 |
| Event sourcing | Mudanças de status do pedido produzem eventos de domínio | P2 |
(2) Modelo de Domínio
// Entidades centrais de domínio
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) Máquina de Estados do Pedido
stateDiagram-v2
[*] --> Pending: Criar Pedido
Pending --> Processing: Iniciar Processamento
Pending --> Cancelled: Cliente Cancela
Processing --> Paid: Pagamento Sucesso
Processing --> Cancelled: Pagamento Falhou
Paid --> Shipped: Enviar Pedido
Shipped --> Delivered: Entrega Confirmada
Delivered --> [*]
Cancelled --> [*]
4. Arquitetura em Camadas
(1) Arquitetura de Quatro Camadas
flowchart TD
A[Camada Controller<br/>API REST / Validação de Requisição] --> B[Camada Service<br/>Lógica de Negócio / Máquina de Estados]
B --> C[Camada Repository<br/>Acesso a Dados / Persistência]
B --> D[Camada Integration<br/>Chamadas a APIs Externas]
C --> E[(Banco de Dados)]
D --> F[Serviço de Pagamento]
D --> G[Serviço de Estoque]
(2) Responsabilidades por Camada
| Camada | Responsabilidade | Tecnologia Chave |
|---|---|---|
| Controller | Tratamento de requisições HTTP, validação, resposta | Spring WebFlux, suspend |
| Service | Lógica de negócio, máquina de estados, publicação de eventos | Coroutines, sealed class |
| Repository | Persistência de dados, consultas | R2DBC, JPA |
| Integration | Chamadas a serviços externos | Ktor Client, Resilience4j |
(3) Estrutura de Módulos do Projeto
order-processor/
├── build.gradle.kts
├── src/main/kotlin/com/order/
│ ├── Application.kt # Main Spring Boot
│ ├── controller/
│ │ └── OrderController.kt # Endpoints REST
│ ├── service/
│ │ ├── OrderService.kt # Lógica de negócio
│ │ └── OrderStateMachine.kt # Transições de estado
│ ├── repository/
│ │ ├── OrderRepository.kt # Acesso a dados
│ │ └── EventRepository.kt # Event store
│ ├── integration/
│ │ ├── PaymentClient.kt # API de pagamento
│ │ └── InventoryClient.kt # API de estoque
│ ├── domain/
│ │ ├── Order.kt # Entidade
│ │ ├── OrderStatus.kt # Enum de status
│ │ └── OrderEvent.kt # Eventos de domínio
│ ├── config/
│ │ └── AppConfig.kt # Configuração
│ └── exception/
│ └── OrderExceptions.kt # Exceções customizadas
└── src/test/kotlin/com/order/
└── ... # Testes
5. Seleção de Stack Tecnológico
| Domínio | Escolha Tecnológica | Motivo |
|---|---|---|
| Framework | Spring Boot 3.2 + WebFlux | Ecossistema maduro, suporte a corrotinas |
| Linguagem | Kotlin 1.9 | Segurança de nulo, corrotinas, data class |
| Async | kotlinx.coroutines | Concorrência estruturada, suspend |
| Serialização | kotlinx.serialization | Segurança em tempo de compilação, múltiplos formatos |
| Banco de dados | PostgreSQL + R2DBC | Driver reativo, amigável a corrotinas |
| Cliente HTTP | Ktor Client | Nativo Kotlin, suporte a corrotinas |
| Cache | Redis + kotlinx.coroutines | Cache assíncrono |
| Testes | JUnit 5 + MockK | Mocking nativo Kotlin |
| Monitoramento | Micrometer + Prometheus | Coleta de métricas |
6. Design de API
(1) Endpoints RESTful
// Endpoints da API
// POST /api/v1/orders - Criar pedido
// GET /api/v1/orders - Listar pedidos (com paginação)
// GET /api/v1/orders/{id} - Buscar pedido por ID
// PATCH /api/v1/orders/{id}/status - Atualizar status do pedido
// DELETE /api/v1/orders/{id} - Cancelar pedido
// GET /api/v1/orders/{id}/events - Obter histórico de eventos do pedido
(2) DTOs de Requisição/Resposta
// Requisição
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
)
// Resposta
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?
)
// Erro
data class ErrorResponse(
val code: String,
val message: String,
val details: Map<String, String>? = null
)
(3) Princípios de Design de API
| Princípio | Prática |
|---|---|
| RESTful | Substantivos de recurso + semântica de métodos HTTP |
| Versionamento | Prefixo /api/v1/ |
| Paginação | ?page=0&size=20 |
| Filtragem | ?status=CONFIRMED&customer=C001 |
| HATEOAS | Incluir links relacionados na resposta (opcional) |
| Formato de erro | ErrorResponse unificado |
7. Esquema de Banco de Dados
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. Exemplo Completo: Documento de Design de Arquitetura do OrderProcessor
// ============================================
// OrderProcessor - Design de Arquitetura
// Funcionalidade: Blueprint completo de arquitetura
// ============================================
// --- Camada de Domínio ---
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>
)
// --- Máquina de Estados ---
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")
}
}
// --- Resumo da Arquitetura ---
fun main() {
println("=== Design de Arquitetura do OrderProcessor ===\n")
println("Stack Tecnológico:")
println(" Framework: Spring Boot 3.2 + WebFlux")
println(" Linguagem: Kotlin 1.9")
println(" Async: kotlinx.coroutines")
println(" Banco: PostgreSQL + R2DBC")
println(" Cache: Redis")
println(" HTTP: Ktor Client")
println(" Testes: JUnit 5 + MockK")
println(" Monitoramento: Micrometer + Prometheus")
println("\nMáquina de Estados do Pedido:")
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("\nEndpoints da API:")
println(" POST /api/v1/orders - Criar pedido")
println(" GET /api/v1/orders - Listar pedidos")
println(" GET /api/v1/orders/{id} - Buscar pedido")
println(" PATCH /api/v1/orders/{id}/status - Atualizar status")
println(" DELETE /api/v1/orders/{id} - Cancelar pedido")
println(" GET /api/v1/orders/{id}/events - Histórico de eventos")
}
Saída:
=== Design de Arquitetura do OrderProcessor ===
Stack Tecnológico:
Framework: Spring Boot 3.2 + WebFlux
Linguagem: Kotlin 1.9
Async: kotlinx.coroutines
Banco: PostgreSQL + R2DBC
Cache: Redis
HTTP: Ktor Client
Testes: JUnit 5 + MockK
Monitoramento: Micrometer + Prometheus
Máquina de Estados do Pedido:
Pending + Confirmed = Confirmed
Confirmed + Shipped = Shipped
Shipped + Delivered = Delivered
Endpoints da API:
POST /api/v1/orders - Criar pedido
GET /api/v1/orders - Listar pedidos
GET /api/v1/orders/{id} - Buscar pedido
PATCH /api/v1/orders/{id}/status - Atualizar status
DELETE /api/v1/orders/{id} - Cancelar pedido
GET /api/v1/orders/{id}/events - Histórico de eventos
9. Exemplos práticos rápidos
▶ Exemplo: Estrutura de projeto em camadas
src/
├── main/kotlin/com/example/orders/
│ ├── domain/ # business models puros
│ │ ├── Order.kt
│ │ ├── OrderStatus.kt
│ │ └── OrderRepository.kt (interface)
│ ├── application/ # use cases
│ │ ├── CreateOrderUseCase.kt
│ │ ├── ConfirmOrderUseCase.kt
│ │ └── CancelOrderUseCase.kt
│ ├── infrastructure/ # implementações técnicas
│ │ ├── persistence/
│ │ │ ├── JpaOrderRepository.kt
│ │ │ └── OrderEntity.kt
│ │ └── web/
│ │ └── OrderController.kt
│ └── OrderApplication.kt
└── test/kotlin/ # testes paralelos
Saída:
Estrutura convencional por camadas:
- domain/ : modelos de negócio puros (Order, OrderStatus, OrderRepository)
- application/: use cases (CreateOrder, ConfirmOrder, CancelOrder)
- infrastructure/: adapters técnicos (JPA, web/REST)
▶ Exemplo: Domain models
// Pure data classes + business invariants
sealed class OrderStatus {
object Created : OrderStatus()
data class Confirmed(val confirmedAt: Long) : OrderStatus()
data class Shipped(val tracking: String) : OrderStatus()
data class Cancelled(val reason: String) : OrderStatus()
}
data class Order(
val id: String,
val customerId: String,
val total: Double,
val status: OrderStatus = OrderStatus.Created,
val createdAt: Long = System.currentTimeMillis()
) {
init {
require(total > 0) { "Total deve ser positivo" }
require(id.startsWith("ORD-")) { "ID inválido" }
}
fun isConfirmed(): Boolean = status is OrderStatus.Confirmed
fun canBeCancelled(): Boolean = status is OrderStatus.Created ||
status is OrderStatus.Confirmed
}
// Repository interface
interface OrderRepository {
suspend fun findById(id: String): Order?
suspend fun save(order: Order): Order
suspend fun list(): List<Order>
}
Saída:
Order(id="ORD-001", customerId="Alice", total=299.99, status=Created)
isConfirmed(): false
canBeCancelled(): true
▶ Exemplo: Application use cases
class ConfirmOrderUseCase(private val repo: OrderRepository, private val notify: Notifier) {
suspend operator fun invoke(orderId: String): Result<Order> {
val order = repo.findById(orderId)
?: return Result.failure(IllegalArgumentException("Pedido $orderId não encontrado"))
if (!order.canBeCancelled()) {
return Result.failure(IllegalStateException("Pedido não pode ser confirmado"))
}
val confirmed = order.copy(
status = OrderStatus.Confirmed(System.currentTimeMillis())
)
val saved = repo.save(confirmed)
notify.orderConfirmed(saved)
return Result.success(saved)
}
}
interface Notifier {
fun orderConfirmed(order: Order)
}
class ConsoleNotifier : Notifier {
override fun orderConfirmed(order: Order) {
println("📧 Notificação: pedido ${order.id} confirmado")
}
}
Saída:
📧 Notificação: pedido ORD-001 confirmado
▶ Exemplo: Injeção de dependência com Koin
import org.koin.dsl.*
val appModule = module {
single<OrderRepository> { JpaOrderRepository() }
single<Notifier> { ConsoleNotifier() }
factory { ConfirmOrderUseCase(get(), get()) }
factory { CreateOrderUseCase(get()) }
factory { CancelOrderUseCase(get(), get()) }
}
// Inicializar app
fun main() {
val koin = startKoin {
modules(appModule)
}
val confirmUseCase = koin.koin.get<ConfirmOrderUseCase>()
runBlocking {
confirmUseCase.invoke("ORD-001")
}
}
Saída:
📧 Notificação: pedido ORD-001 confirmado
[Container de DI inicializado; UseCase executado via Koin]
▶ Exemplo: Padrão Result para erros
sealed class OrderError(message: String) : Exception(message) {
object NotFound : OrderError("Pedido não encontrado")
data class InvalidStatus(val current: String, val required: String) :
OrderError("Status atual '$current' não permite '$required'")
data class ValidationError(override val message: String) : OrderError(message)
}
class OrderService(private val repo: OrderRepository) {
suspend fun confirm(id: String): Result<Order> {
val order = repo.findById(id) ?: return Result.failure(OrderError.NotFound)
if (order.status !is OrderStatus.Created) {
return Result.failure(
OrderError.InvalidStatus(
current = order.status.toString(),
required = "confirm"
)
)
}
val confirmed = order.copy(
status = OrderStatus.Confirmed(System.currentTimeMillis())
)
return Result.success(repo.save(confirmed))
}
}
// Uso
runBlocking {
val result = orderService.confirm("ORD-001")
result.fold(
onSuccess = { println("✅ Confirmado: $it") },
onFailure = { println("❌ Erro: ${it.message}") }
)
}
Saída:
✅ Confirmado: Order(id=ORD-001, status=Confirmed(confirmedAt=...))
▶ Exemplo: API design com versionamento
// Versionamento via URL path
@RestController
@RequestMapping("/api/v1/orders")
class OrderControllerV1(private val service: OrderService) {
@GetMapping
suspend fun list(): ResponseEntity<List<OrderDTO>> = ResponseEntity.ok(
service.list().map { it.toV1DTO() }
)
}
// V2 com novos campos
@RestController
@RequestMapping("/api/v2/orders")
class OrderControllerV2(private val service: OrderService) {
@GetMapping
suspend fun list(): ResponseEntity<List<OrderDTOV2>> = ResponseEntity.ok(
service.list().map { it.toV2DTO() }
)
}
data class OrderDTO(
val id: String,
val total: Double,
val status: String
)
data class OrderDTOV2(
val id: String,
val total: Double,
val status: String,
val createdAt: Long,
val customerId: String, // novo
val events: List<OrderEventDTO> // novo
)
Saída:
GET /api/v1/orders -> OrderDTO[ ] (versão legacy)
GET /api/v2/orders -> OrderDTOV2[ ] (com customerId, events)
▶ Exemplo: Testes para o design
// Testes para o domínio puro
class OrderTest {
@Test
fun `total deve ser positivo`() {
assertThrows<IllegalArgumentException> {
Order(id = "ORD-001", customerId = "Alice", total = -1.0)
}
}
@Test
fun `pedido novo pode ser cancelado`() {
val order = Order("ORD-001", "Alice", 299.99)
assertTrue(order.canBeCancelled())
}
@Test
fun `pedido enviado não pode ser cancelado`() {
val order = Order(
"ORD-001", "Alice", 299.99,
status = OrderStatus.Shipped("TRK-123")
)
assertFalse(order.canBeCancelled())
}
}
Saída:
Testes unitários cobrindo:
- invariants do construtor
- transições válidas de status
- regras de cancelamento
❓ Perguntas Frequentes
P: Projetar o banco de dados primeiro ou a API primeiro? R: Projetar o modelo de domínio primeiro (abordagem DDD). Então tanto a API quanto o banco de dados são projeções do modelo de domínio. O modelo de domínio é central; API e Schema são periféricos.
P: Qual a diferença entre event sourcing e CRUD? R: CRUD armazena apenas o estado atual. Event sourcing armazena todos os eventos de mudança de estado. Event sourcing suporta auditoria, replay e time-travel, mas tem complexidade maior. Comece com CRUD; adicione eventos quando necessário.
P: R2DBC ou JPA — como escolher? R: Novo projeto + WebFlux + corrotinas → R2DBC (não-bloqueante). Projeto JPA existente ou equipe não familiarizada com reativo → JPA + extensões de corrotinas.
P: Quão pequeno deve ser um microsserviço? R: Um microsserviço por contexto delimitado. OrderProcessor é o contexto de "processamento de pedidos" — não enfie pagamento e estoque nele também.
P: Como lidar com versionamento de API? R: Versionamento por caminho de URL (
/api/v1/) é o mais simples e intuitivo. Versionamento por header é mais RESTful mas mais complexo. Versionamento por URL é recomendado.
P: Como garantir que o design de arquitetura é implementável? R: Toda decisão arquitetural deve ter um código POC (prova de conceito). Arquitetura no papel + protótipo executável = design implementável.
📖 Resumo
- Requisitos → Modelagem de domínio → Máquina de estados → Arquitetura → Stack tecnológico → API → Banco de dados
- Arquitetura em camadas: Controller → Service → Repository → Integration
- Sealed classes modelam a máquina de estados do pedido com verificações exaustivas pelo compilador
- Stack tecnológico: Spring Boot + WebFlux + Coroutines + R2DBC
- Design de API RESTful: substantivos de recurso + métodos HTTP + versionamento + formato de erro unificado
- Event sourcing: armazena eventos de mudança de estado para auditoria e replay
📝 Exercícios
- Iniciante (⭐): Use
sealed classpara projetar uma máquina de estados para um sistema simples de gerenciamento de tarefas (Todo → InProgress → Done / Cancelled). Dica:sealed class TaskStatus - Intermediário (⭐⭐): Desenvolva um contrato de API RESTful completo com data classes de requisição/resposta para 5 endpoints. Dica:
CreateOrderRequest/OrderResponse - Avançado (⭐⭐⭐): Desenvolva um documento completo de arquitetura em camadas para um sistema de e-commerce, incluindo modelo de domínio, máquina de estados, API, esquema de banco de dados e stack tecnológico. Dica: Consulte todas as seções desta lição