Clean Architecture (Arquitetura Limpa) é o padrão usado por empresas como Netflix, Uber e Google para construir sistemas escaláveis e testáveis. Em Go, ela brilha pela simplicidade que combina com a filosofia da linguagem. Neste guia, você vai aprender a organizar projetos Go profissionais.
Por Que Clean Architecture em Go?
Os Problemas Sem Arquitetura
📁 projeto-caotico/
├── main.go # 2000 linhas
├── handlers.go # Tudo misturado
├── database.go # SQL espalhado
├── models.go # Regras de negócio aqui
└── utils.go # Deus sabe o que tem aqui
Problemas:
- ❌ Código difícil de testar - Tudo acoplado
- ❌ Mudanças dolorosas - Alterar DB quebra handlers
- ❌ Regras de negócio espalhadas - Onde fica a lógica?
- ❌ Duplicação de código - Mesma query em 5 lugares
- ❌ Não escala - Equipe trava no código legado
A Solução: Clean Architecture
📁 projeto-organizado/
├── cmd/
│ └── api/
│ └── main.go # Entry point (30 linhas)
├── internal/
│ ├── domain/ # Regras de negócio puras
│ ├── application/ # Casos de uso
│ ├── infrastructure/ # Detalhes (DB, HTTP, etc)
│ └── ports/ # Interfaces/contratos
└── pkg/ # Bibliotecas compartilhadas
Benefícios:
- ✅ Testável - Mock fácil de dependências
- ✅ Flexível - Troca de DB sem quebrar nada
- ✅ Regras de negócio protegidas - Isoladas do framework
- ✅ Escalável - Times trabalham em paralelo
- ✅ Manutenível - Cada camada tem responsabilidade única
Os Princípios da Clean Architecture
A Regra da Dependência
┌─────────────────────┐
│ Camada Externa │ ← Frameworks, UI, DB
│ (Infrastructure) │ Depende de internas
├─────────────────────┤
│ Camada de Casos │ ← Application
│ de Uso │ Orquestra fluxo
├─────────────────────┤
│ Camada de Entidades│ ← Domain
│ (Regras de Negócio)│ O coração do sistema
└─────────────────────┘
⬆️ Dependências apontam para dentro
⬇️ Camadas internas NÃO conhecem externas
As Camadas Explicadas
| Camada | Responsabilidade | Exemplo |
|---|---|---|
| Domain | Entidades e regras de negócio puro | Order, User, Validate() |
| Application | Casos de uso, orquestração | CreateOrderUseCase |
| Infrastructure | Frameworks, DB, HTTP, filas | PostgresRepository, GinHandler |
| Ports | Interfaces que conectam camadas | OrderRepository, UserService |
Estrutura de Projeto Go
Layout Completo
📦 order-service/
│
├── 📁 cmd/
│ └── 📁 api/
│ └── main.go # Entry point da aplicação
│
├── 📁 internal/
│ │
│ ├── 📁 domain/ # Regras de negócio (independentes)
│ │ ├── order.go # Entidade Order
│ │ ├── user.go # Entidade User
│ │ └── errors.go # Erros de domínio
│ │
│ ├── 📁 application/ # Casos de uso
│ │ ├── dto.go # Data Transfer Objects
│ │ ├── order_usecase.go # Criar pedido
│ │ └── user_usecase.go # Gerenciar usuário
│ │
│ ├── 📁 ports/ # Interfaces (contratos)
│ │ ├── repository.go # Portas de repositório
│ │ └── service.go # Portas de serviço
│ │
│ ├── 📁 infrastructure/ # Implementações concretas
│ │ ├── 📁 persistence/ # Banco de dados
│ │ │ ├── postgres/
│ │ │ │ └── order_repository.go
│ │ │ └── redis/
│ │ │ └── cache.go
│ │ ├── 📁 http/ # HTTP handlers
│ │ │ ├── handler.go
│ │ │ ├── router.go
│ │ │ └── middleware.go
│ │ ├── 📁 messaging/ # Fila/mensageria
│ │ │ └── kafka/
│ │ │ └── publisher.go
│ │ └── 📁 external/ # APIs externas
│ │ └── payment_gateway.go
│ │
│ └── 📁 config/ # Configurações
│ └── config.go
│
├── 📁 pkg/ # Código compartilhado
│ ├── logger/
│ ├── validator/
│ └── errors/
│
├── 📁 api/ # Contratos de API
│ └── openapi.yaml
│
├── 📁 migrations/ # Migrações de DB
│ └── 001_create_orders.sql
│
├── 📁 deployments/ # Infra
│ ├── docker/
│ └── k8s/
│
├── go.mod
├── go.sum
└── Makefile
Por Que Usar internal/?
O diretório internal/ tem significado especial em Go:
- Pacotes em
internal/só podem ser importados pelo módulo atual - Impede acoplamento acidental entre serviços
- Protege a arquitetura de violações
// ✅ Pode importar (mesmo módulo)
import "github.com/example/order-service/internal/domain"
// ❌ Erro de compilação (outro módulo)
import "github.com/example/other-service/internal/domain"
// "use of internal package not allowed"
Implementação Prática
1. Camada de Domínio (Domain)
O coração do sistema. Não tem dependências externas.
// internal/domain/order.go
package domain
import (
"errors"
"time"
)
// Erros de domínio
var (
ErrInvalidAmount = errors.New("valor do pedido deve ser positivo")
ErrEmptyItems = errors.New("pedido deve ter pelo menos um item")
ErrInvalidStatus = errors.New("transição de status inválida")
ErrOrderNotFound = errors.New("pedido não encontrado")
ErrInsufficientStock = errors.New("estoque insuficiente")
)
// Order é a entidade central do domínio
type Order struct {
ID string
UserID string
Items []OrderItem
Total Money
Status OrderStatus
CreatedAt time.Time
UpdatedAt time.Time
}
// OrderItem representa um item do pedido
type OrderItem struct {
ProductID string
ProductName string
Quantity int
UnitPrice Money
}
// Money representa valores monetários (evita float)
type Money struct {
Amount int64 // Valor em centavos (evita problema de precisão)
Currency string // "BRL", "USD"
}
// OrderStatus é uma enumeração tipada
type OrderStatus string
const (
OrderStatusPending OrderStatus = "PENDING"
OrderStatusPaid OrderStatus = "PAID"
OrderStatusShipped OrderStatus = "SHIPPED"
OrderStatusDelivered OrderStatus = "DELIVERED"
OrderStatusCancelled OrderStatus = "CANCELLED"
)
// Métodos de domínio - regras de negócio puras
// CalculateTotal recalcula o total do pedido
func (o *Order) CalculateTotal() {
var total int64
for _, item := range o.Items {
total += item.UnitPrice.Amount * int64(item.Quantity)
}
o.Total = Money{
Amount: total,
Currency: "BRL",
}
}
// Validate verifica se o pedido é válido
func (o *Order) Validate() error {
if o.Total.Amount <= 0 {
return ErrInvalidAmount
}
if len(o.Items) == 0 {
return ErrEmptyItems
}
for _, item := range o.Items {
if item.Quantity <= 0 {
return errors.New("quantidade deve ser positiva")
}
}
return nil
}
// Pay muda o status para pago
func (o *Order) Pay() error {
if o.Status != OrderStatusPending {
return ErrInvalidStatus
}
o.Status = OrderStatusPaid
o.UpdatedAt = time.Now()
return nil
}
// Ship muda o status para enviado
func (o *Order) Ship() error {
if o.Status != OrderStatusPaid {
return ErrInvalidStatus
}
o.Status = OrderStatusShipped
o.UpdatedAt = time.Now()
return nil
}
// Cancel cancela o pedido
func (o *Order) Cancel() error {
if o.Status != OrderStatusPending {
return ErrInvalidStatus
}
o.Status = OrderStatusCancelled
o.UpdatedAt = time.Now()
return nil
}
// CanTransitionTo verifica se pode mudar para um status
func (o *Order) CanTransitionTo(newStatus OrderStatus) bool {
transitions := map[OrderStatus][]OrderStatus{
OrderStatusPending: {OrderStatusPaid, OrderStatusCancelled},
OrderStatusPaid: {OrderStatusShipped},
OrderStatusShipped: {OrderStatusDelivered},
OrderStatusDelivered: {},
OrderStatusCancelled: {},
}
allowed, ok := transitions[o.Status]
if !ok {
return false
}
for _, s := range allowed {
if s == newStatus {
return true
}
}
return false
}
// AddItem adiciona um item ao pedido
func (o *Order) AddItem(item OrderItem) error {
if item.Quantity <= 0 {
return errors.New("quantidade deve ser positiva")
}
if item.UnitPrice.Amount <= 0 {
return errors.New("preço deve ser positivo")
}
o.Items = append(o.Items, item)
o.CalculateTotal()
o.UpdatedAt = time.Now()
return nil
}
// RemoveItem remove um item do pedido
func (o *Order) RemoveItem(productID string) error {
for i, item := range o.Items {
if item.ProductID == productID {
o.Items = append(o.Items[:i], o.Items[i+1:]...)
o.CalculateTotal()
o.UpdatedAt = time.Now()
return nil
}
}
return errors.New("item não encontrado")
}
// IsFinal retorna true se o pedido está em status final
func (o *Order) IsFinal() bool {
return o.Status == OrderStatusDelivered || o.Status == OrderStatusCancelled
}
// internal/domain/user.go
package domain
import (
"errors"
"regexp"
"time"
)
var (
ErrInvalidEmail = errors.New("email inválido")
ErrInvalidName = errors.New("nome deve ter pelo menos 3 caracteres")
ErrUserInactive = errors.New("usuário inativo")
)
type User struct {
ID string
Name string
Email string
Active bool
CreatedAt time.Time
UpdatedAt time.Time
}
func (u *User) Validate() error {
if len(u.Name) < 3 {
return ErrInvalidName
}
if !isValidEmail(u.Email) {
return ErrInvalidEmail
}
return nil
}
func (u *User) CanPlaceOrder() error {
if !u.Active {
return ErrUserInactive
}
return nil
}
func isValidEmail(email string) bool {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
return matched
}
2. Ports (Interfaces)
Define contratos que a aplicação precisa. Não implementa nada.
// internal/ports/repository.go
package ports
import (
"context"
"order-service/internal/domain"
)
// OrderRepository define o contrato para persistência de pedidos
// A camada de domínio não sabe (e não se importa) se é Postgres, Mongo, etc.
type OrderRepository interface {
// Create salva um novo pedido
Create(ctx context.Context, order *domain.Order) error
// GetByID busca um pedido pelo ID
GetByID(ctx context.Context, id string) (*domain.Order, error)
// GetByUserID busca pedidos de um usuário
GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*domain.Order, error)
// Update atualiza um pedido existente
Update(ctx context.Context, order *domain.Order) error
// Delete remove um pedido
Delete(ctx context.Context, id string) error
// UpdateStatus atualiza apenas o status (otimização)
UpdateStatus(ctx context.Context, id string, status domain.OrderStatus) error
}
// UserRepository define o contrato para persistência de usuários
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
GetByID(ctx context.Context, id string) (*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
}
// internal/ports/service.go
package ports
import (
"context"
"order-service/internal/domain"
)
// PaymentService define o contrato para processamento de pagamentos
// Pode ser implementado por Stripe, PagSeguro, MercadoPago, etc.
type PaymentService interface {
// ProcessPayment processa o pagamento de um pedido
ProcessPayment(ctx context.Context, orderID string, amount domain.Money) (*PaymentResult, error)
// Refund solicita reembolso
Refund(ctx context.Context, paymentID string) error
// GetPaymentStatus consulta status do pagamento
GetPaymentStatus(ctx context.Context, paymentID string) (PaymentStatus, error)
}
type PaymentResult struct {
PaymentID string
Status PaymentStatus
Receipt string
}
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "PENDING"
PaymentStatusApproved PaymentStatus = "APPROVED"
PaymentStatusDeclined PaymentStatus = "DECLINED"
PaymentStatusRefunded PaymentStatus = "REFUNDED"
)
// NotificationService define contrato para notificações
type NotificationService interface {
SendOrderConfirmation(ctx context.Context, order *domain.Order) error
SendShippingNotification(ctx context.Context, order *domain.Order, trackingID string) error
}
// InventoryService define contrato para estoque
type InventoryService interface {
ReserveItems(ctx context.Context, items []domain.OrderItem) error
ReleaseItems(ctx context.Context, items []domain.OrderItem) error
CheckAvailability(ctx context.Context, productID string, quantity int) (bool, error)
}
3. Camada de Aplicação (Use Cases)
Orquestra o fluxo de dados entre domínio e infraestrutura.
// internal/application/dto.go
package application
import "order-service/internal/domain"
// DTOs (Data Transfer Objects) - usados na entrada/saída
// CreateOrderInput dados para criar pedido
type CreateOrderInput struct {
UserID string `json:"user_id" validate:"required"`
Items []OrderItemInput `json:"items" validate:"required,min=1"`
}
type OrderItemInput struct {
ProductID string `json:"product_id" validate:"required"`
ProductName string `json:"product_name" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
UnitPrice int64 `json:"unit_price" validate:"required,gt=0"`
}
// OrderOutput formato de saída
type OrderOutput struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Items []OrderItemOutput `json:"items"`
Total MoneyOutput `json:"total"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
type OrderItemOutput struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
Quantity int `json:"quantity"`
UnitPrice MoneyOutput `json:"unit_price"`
}
type MoneyOutput struct {
Amount int64 `json:"amount"`
Currency string `json:"currency"`
}
// Mapeadores entre DTO e Domínio
func toDomainItems(items []OrderItemInput) []domain.OrderItem {
result := make([]domain.OrderItem, len(items))
for i, item := range items {
result[i] = domain.OrderItem{
ProductID: item.ProductID,
ProductName: item.ProductName,
Quantity: item.Quantity,
UnitPrice: domain.Money{
Amount: item.UnitPrice,
Currency: "BRL",
},
}
}
return result
}
func toOutputOrder(order *domain.Order) OrderOutput {
items := make([]OrderItemOutput, len(order.Items))
for i, item := range order.Items {
items[i] = OrderItemOutput{
ProductID: item.ProductID,
ProductName: item.ProductName,
Quantity: item.Quantity,
UnitPrice: MoneyOutput{
Amount: item.UnitPrice.Amount,
Currency: item.UnitPrice.Currency,
},
}
}
return OrderOutput{
ID: order.ID,
UserID: order.UserID,
Items: items,
Total: MoneyOutput{
Amount: order.Total.Amount,
Currency: order.Total.Currency,
},
Status: string(order.Status),
CreatedAt: order.CreatedAt.Format(time.RFC3339),
}
}
// internal/application/order_usecase.go
package application
import (
"context"
"fmt"
"time"
"order-service/internal/domain"
"order-service/internal/ports"
"order-service/pkg/logger"
)
// OrderUseCase implementa os casos de uso de pedidos
type OrderUseCase struct {
// Dependências injetadas via construtor
orderRepo ports.OrderRepository
userRepo ports.UserRepository
paymentSvc ports.PaymentService
inventorySvc ports.InventoryService
notifier ports.NotificationService
logger logger.Logger
}
// NewOrderUseCase cria uma nova instância
// Dependency Injection - recebe interfaces, não implementações
func NewOrderUseCase(
orderRepo ports.OrderRepository,
userRepo ports.UserRepository,
paymentSvc ports.PaymentService,
inventorySvc ports.InventoryService,
notifier ports.NotificationService,
logger logger.Logger,
) *OrderUseCase {
return &OrderUseCase{
orderRepo: orderRepo,
userRepo: userRepo,
paymentSvc: paymentSvc,
inventorySvc: inventorySvc,
notifier: notifier,
logger: logger,
}
}
// CreateOrder executa o caso de uso de criação de pedido
func (uc *OrderUseCase) CreateOrder(ctx context.Context, input CreateOrderInput) (*OrderOutput, error) {
uc.logger.Info("iniciando criação de pedido", "user_id", input.UserID)
// 1. Validar usuário
user, err := uc.userRepo.GetByID(ctx, input.UserID)
if err != nil {
uc.logger.Error("usuário não encontrado", "error", err)
return nil, fmt.Errorf("usuário não encontrado: %w", err)
}
if err := user.CanPlaceOrder(); err != nil {
uc.logger.Warn("usuário não pode fazer pedidos", "user_id", user.ID, "error", err)
return nil, err
}
// 2. Verificar estoque
for _, item := range input.Items {
available, err := uc.inventorySvc.CheckAvailability(ctx, item.ProductID, item.Quantity)
if err != nil {
uc.logger.Error("erro ao verificar estoque", "product_id", item.ProductID, "error", err)
return nil, fmt.Errorf("erro no estoque: %w", err)
}
if !available {
uc.logger.Warn("produto sem estoque", "product_id", item.ProductID)
return nil, domain.ErrInsufficientStock
}
}
// 3. Criar entidade de domínio
order := &domain.Order{
ID: generateID(),
UserID: input.UserID,
Items: toDomainItems(input.Items),
Status: domain.OrderStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
order.CalculateTotal()
// 4. Validar regras de domínio
if err := order.Validate(); err != nil {
uc.logger.Error("validação de pedido falhou", "error", err)
return nil, err
}
// 5. Reservar estoque
if err := uc.inventorySvc.ReserveItems(ctx, order.Items); err != nil {
uc.logger.Error("falha ao reservar estoque", "error", err)
return nil, fmt.Errorf("erro ao reservar estoque: %w", err)
}
// 6. Persistir pedido
if err := uc.orderRepo.Create(ctx, order); err != nil {
// Rollback: liberar estoque
uc.inventorySvc.ReleaseItems(ctx, order.Items)
uc.logger.Error("falha ao salvar pedido", "error", err)
return nil, fmt.Errorf("erro ao criar pedido: %w", err)
}
// 7. Enviar notificação (não bloqueante)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := uc.notifier.SendOrderConfirmation(ctx, order); err != nil {
uc.logger.Error("falha ao enviar notificação", "error", err)
}
}()
uc.logger.Info("pedido criado com sucesso", "order_id", order.ID)
output := toOutputOrder(order)
return &output, nil
}
// PayOrder processa o pagamento de um pedido
func (uc *OrderUseCase) PayOrder(ctx context.Context, orderID string) (*OrderOutput, error) {
// 1. Buscar pedido
order, err := uc.orderRepo.GetByID(ctx, orderID)
if err != nil {
return nil, fmt.Errorf("pedido não encontrado: %w", err)
}
// 2. Validar transição de status
if !order.CanTransitionTo(domain.OrderStatusPaid) {
return nil, domain.ErrInvalidStatus
}
// 3. Processar pagamento
paymentResult, err := uc.paymentSvc.ProcessPayment(ctx, orderID, order.Total)
if err != nil {
uc.logger.Error("falha no pagamento", "order_id", orderID, "error", err)
return nil, fmt.Errorf("pagamento falhou: %w", err)
}
if paymentResult.Status != ports.PaymentStatusApproved {
return nil, fmt.Errorf("pagamento não aprovado: %s", paymentResult.Status)
}
// 4. Atualizar domínio
if err := order.Pay(); err != nil {
return nil, err
}
// 5. Persistir
if err := uc.orderRepo.Update(ctx, order); err != nil {
return nil, fmt.Errorf("falha ao atualizar pedido: %w", err)
}
output := toOutputOrder(order)
return &output, nil
}
// GetOrder busca um pedido
func (uc *OrderUseCase) GetOrder(ctx context.Context, orderID string) (*OrderOutput, error) {
order, err := uc.orderRepo.GetByID(ctx, orderID)
if err != nil {
return nil, err
}
output := toOutputOrder(order)
return &output, nil
}
// CancelOrder cancela um pedido
func (uc *OrderUseCase) CancelOrder(ctx context.Context, orderID string) error {
order, err := uc.orderRepo.GetByID(ctx, orderID)
if err != nil {
return err
}
if err := order.Cancel(); err != nil {
return err
}
// Liberar estoque
if err := uc.inventorySvc.ReleaseItems(ctx, order.Items); err != nil {
uc.logger.Error("falha ao liberar estoque", "error", err)
}
return uc.orderRepo.Update(ctx, order)
}
func generateID() string {
// Implementação simples - em produção use UUID
return fmt.Sprintf("ORD-%d", time.Now().UnixNano())
}
4. Infraestrutura (Implementações)
// internal/infrastructure/persistence/postgres/order_repository.go
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
_ "github.com/lib/pq"
"order-service/internal/domain"
"order-service/internal/ports"
)
// OrderRepository implementa ports.OrderRepository para PostgreSQL
type OrderRepository struct {
db *sql.DB
}
// Garante que implementa a interface
var _ ports.OrderRepository = (*OrderRepository)(nil)
// NewOrderRepository cria uma nova instância
func NewOrderRepository(db *sql.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error {
// Serializar items para JSON
itemsJSON, err := json.Marshal(order.Items)
if err != nil {
return fmt.Errorf("falha ao serializar items: %w", err)
}
query := `
INSERT INTO orders (id, user_id, items, total_amount, total_currency, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
_, err = r.db.ExecContext(ctx, query,
order.ID,
order.UserID,
itemsJSON,
order.Total.Amount,
order.Total.Currency,
order.Status,
order.CreatedAt,
order.UpdatedAt,
)
if err != nil {
return fmt.Errorf("falha ao inserir pedido: %w", err)
}
return nil
}
func (r *OrderRepository) GetByID(ctx context.Context, id string) (*domain.Order, error) {
query := `
SELECT id, user_id, items, total_amount, total_currency, status, created_at, updated_at
FROM orders
WHERE id = $1
`
var order domain.Order
var itemsJSON []byte
err := r.db.QueryRowContext(ctx, query, id).Scan(
&order.ID,
&order.UserID,
&itemsJSON,
&order.Total.Amount,
&order.Total.Currency,
&order.Status,
&order.CreatedAt,
&order.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, domain.ErrOrderNotFound
}
if err != nil {
return nil, fmt.Errorf("falha ao buscar pedido: %w", err)
}
// Desserializar items
if err := json.Unmarshal(itemsJSON, &order.Items); err != nil {
return nil, fmt.Errorf("falha ao desserializar items: %w", err)
}
return &order, nil
}
func (r *OrderRepository) GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*domain.Order, error) {
offset := (page - 1) * pageSize
query := `
SELECT id, user_id, items, total_amount, total_currency, status, created_at, updated_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := r.db.QueryContext(ctx, query, userID, pageSize, offset)
if err != nil {
return nil, err
}
defer rows.Close()
return r.scanOrders(rows)
}
func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error {
itemsJSON, _ := json.Marshal(order.Items)
query := `
UPDATE orders
SET items = $1, total_amount = $2, status = $3, updated_at = $4
WHERE id = $5
`
result, err := r.db.ExecContext(ctx, query,
itemsJSON,
order.Total.Amount,
order.Status,
order.UpdatedAt,
order.ID,
)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
return domain.ErrOrderNotFound
}
return nil
}
func (r *OrderRepository) Delete(ctx context.Context, id string) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM orders WHERE id = $1", id)
return err
}
func (r *OrderRepository) UpdateStatus(ctx context.Context, id string, status domain.OrderStatus) error {
_, err := r.db.ExecContext(ctx,
"UPDATE orders SET status = $1, updated_at = NOW() WHERE id = $2",
status, id,
)
return err
}
// Helper para scan múltiplas rows
func (r *OrderRepository) scanOrders(rows *sql.Rows) ([]*domain.Order, error) {
var orders []*domain.Order
for rows.Next() {
var order domain.Order
var itemsJSON []byte
err := rows.Scan(
&order.ID,
&order.UserID,
&itemsJSON,
&order.Total.Amount,
&order.Total.Currency,
&order.Status,
&order.CreatedAt,
&order.UpdatedAt,
)
if err != nil {
return nil, err
}
json.Unmarshal(itemsJSON, &order.Items)
orders = append(orders, &order)
}
return orders, rows.Err()
}
// internal/infrastructure/http/handler.go
package http
import (
"encoding/json"
"net/http"
"order-service/internal/application"
"order-service/pkg/errors"
)
// OrderHandler lida com requisições HTTP de pedidos
type OrderHandler struct {
useCase *application.OrderUseCase
}
func NewOrderHandler(useCase *application.OrderUseCase) *OrderHandler {
return &OrderHandler{useCase: useCase}
}
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var input application.CreateOrderInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
h.respondError(w, http.StatusBadRequest, "JSON inválido")
return
}
// Executar caso de uso
output, err := h.useCase.CreateOrder(ctx, input)
if err != nil {
switch {
case errors.Is(err, domain.ErrInsufficientStock):
h.respondError(w, http.StatusConflict, "Estoque insuficiente")
case errors.Is(err, domain.ErrInvalidAmount):
h.respondError(w, http.StatusBadRequest, "Valor inválido")
default:
h.respondError(w, http.StatusInternalServerError, "Erro interno")
}
return
}
h.respondJSON(w, http.StatusCreated, output)
}
func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orderID := r.URL.Query().Get("id")
if orderID == "" {
h.respondError(w, http.StatusBadRequest, "ID do pedido obrigatório")
return
}
output, err := h.useCase.GetOrder(ctx, orderID)
if err != nil {
if errors.Is(err, domain.ErrOrderNotFound) {
h.respondError(w, http.StatusNotFound, "Pedido não encontrado")
return
}
h.respondError(w, http.StatusInternalServerError, "Erro interno")
return
}
h.respondJSON(w, http.StatusOK, output)
}
func (h *OrderHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func (h *OrderHandler) respondError(w http.ResponseWriter, status int, message string) {
h.respondJSON(w, status, map[string]string{"error": message})
}
5. Wire Everything (Dependency Injection)
// cmd/api/main.go
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/lib/pq"
"order-service/internal/application"
httphandler "order-service/internal/infrastructure/http"
"order-service/internal/infrastructure/persistence/postgres"
"order-service/pkg/logger"
)
func main() {
// Configurações
dbURL := "postgres://user:pass@localhost/orderdb?sslmode=disable"
port := ":8080"
// 1. Inicializar infraestrutura (fora -> dentro)
log := logger.NewZapLogger()
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatal("falha ao conectar no banco", "error", err)
}
defer db.Close()
// 2. Criar repositórios
orderRepo := postgres.NewOrderRepository(db)
userRepo := postgres.NewUserRepository(db)
// 3. Criar serviços externos (stubs para exemplo)
paymentSvc := NewMockPaymentService()
inventorySvc := NewMockInventoryService()
notifier := NewMockNotificationService()
// 4. Criar casos de uso (injetando dependências)
orderUseCase := application.NewOrderUseCase(
orderRepo,
userRepo,
paymentSvc,
inventorySvc,
notifier,
log,
)
// 5. Criar handlers HTTP
orderHandler := httphandler.NewOrderHandler(orderUseCase)
// 6. Configurar rotas
mux := http.NewServeMux()
mux.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
orderHandler.CreateOrder(w, r)
case http.MethodGet:
orderHandler.GetOrder(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
})
log.Info("servidor iniciando", "port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal("falha no servidor", "error", err)
}
}
Testes na Clean Architecture
Testando Casos de Uso (Mocks)
// internal/application/order_usecase_test.go
package application
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"order-service/internal/domain"
"order-service/internal/ports"
)
// Mock do repositório
type MockOrderRepository struct {
mock.Mock
}
func (m *MockOrderRepository) Create(ctx context.Context, order *domain.Order) error {
args := m.Called(ctx, order)
return args.Error(0)
}
func (m *MockOrderRepository) GetByID(ctx context.Context, id string) (*domain.Order, error) {
args := m.Called(ctx, id)
return args.Get(0).(*domain.Order), args.Error(1)
}
// ... implementar outros métodos
func TestOrderUseCase_CreateOrder(t *testing.T) {
// Arrange
mockOrderRepo := new(MockOrderRepository)
mockUserRepo := new(MockUserRepository)
mockPaymentSvc := new(MockPaymentService)
mockInventorySvc := new(MockInventoryService)
mockNotifier := new(MockNotificationService)
mockLogger := new(MockLogger)
useCase := NewOrderUseCase(
mockOrderRepo,
mockUserRepo,
mockPaymentSvc,
mockInventorySvc,
mockNotifier,
mockLogger,
)
input := CreateOrderInput{
UserID: "user-123",
Items: []OrderItemInput{
{ProductID: "prod-1", ProductName: "Produto 1", Quantity: 2, UnitPrice: 5000},
},
}
// Configurar expectativas
mockUserRepo.On("GetByID", mock.Anything, "user-123").
Return(&domain.User{ID: "user-123", Active: true}, nil)
mockInventorySvc.On("CheckAvailability", mock.Anything, "prod-1", 2).
Return(true, nil)
mockInventorySvc.On("ReserveItems", mock.Anything, mock.Anything).
Return(nil)
mockOrderRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.Order")).
Return(nil)
// Act
output, err := useCase.CreateOrder(context.Background(), input)
// Assert
assert.NoError(t, err)
assert.NotNil(t, output)
assert.Equal(t, "user-123", output.UserID)
assert.Equal(t, int64(10000), output.Total.Amount) // 2 * 5000
// Verificar que métodos foram chamados
mockOrderRepo.AssertExpectations(t)
mockInventorySvc.AssertExpectations(t)
}
func TestOrderUseCase_CreateOrder_UserInactive(t *testing.T) {
mockOrderRepo := new(MockOrderRepository)
mockUserRepo := new(MockUserRepository)
mockLogger := new(MockLogger)
useCase := NewOrderUseCase(
mockOrderRepo, mockUserRepo, nil, nil, nil, mockLogger,
)
input := CreateOrderInput{UserID: "user-123", Items: []OrderItemInput{{ProductID: "p1", Quantity: 1}}}
// Usuário inativo
mockUserRepo.On("GetByID", mock.Anything, "user-123").
Return(&domain.User{ID: "user-123", Active: false}, nil)
output, err := useCase.CreateOrder(context.Background(), input)
assert.Error(t, err)
assert.Equal(t, domain.ErrUserInactive, err)
assert.Nil(t, output)
}
// Testando regras de domínio (sem mocks!)
func TestOrder_CalculateTotal(t *testing.T) {
order := &domain.Order{
Items: []domain.OrderItem{
{Quantity: 2, UnitPrice: domain.Money{Amount: 1000, Currency: "BRL"}},
{Quantity: 1, UnitPrice: domain.Money{Amount: 500, Currency: "BRL"}},
},
}
order.CalculateTotal()
assert.Equal(t, int64(2500), order.Total.Amount) // 2*1000 + 1*500
assert.Equal(t, "BRL", order.Total.Currency)
}
func TestOrder_StatusTransitions(t *testing.T) {
tests := []struct {
name string
from domain.OrderStatus
to domain.OrderStatus
shouldWork bool
}{
{"Pending -> Paid", domain.OrderStatusPending, domain.OrderStatusPaid, true},
{"Pending -> Shipped", domain.OrderStatusPending, domain.OrderStatusShipped, false},
{"Paid -> Shipped", domain.OrderStatusPaid, domain.OrderStatusShipped, true},
{"Shipped -> Paid", domain.OrderStatusShipped, domain.OrderStatusPaid, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
order := &domain.Order{Status: tt.from}
can := order.CanTransitionTo(tt.to)
assert.Equal(t, tt.shouldWork, can)
})
}
}
Resumo da Arquitetura
┌─────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE │
│ (HTTP Handlers, Postgres, Stripe, Kafka, Redis) │
│ │
│ Depende de: Application │
└─────────────────────────────────────────────────────────────┘
▲
│ Usa interfaces (Ports)
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION │
│ (OrderUseCase, UserUseCase) │
│ │
│ Depende de: Domain, Ports │
└─────────────────────────────────────────────────────────────┘
▲
│ Usa entidades
┌─────────────────────────────────────────────────────────────┐
│ DOMAIN │
│ (Order, User - regras de negócio puras) │
│ │
│ Não depende de ninguém! │
└─────────────────────────────────────────────────────────────┘
Próximos Passos
Continue seu aprendizado:
- Go e gRPC - Comunicação entre serviços
- Go para Microserviços - Arquitetura distribuída
- Go Testing - Testes avançados
- Go para APIs REST - Fundamentos HTTP
Clean Architecture em Go: código limpo, testável e escalável. Compartilhe sua estrutura!