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

CamadaResponsabilidadeExemplo
DomainEntidades e regras de negócio puroOrder, User, Validate()
ApplicationCasos de uso, orquestraçãoCreateOrderUseCase
InfrastructureFrameworks, DB, HTTP, filasPostgresRepository, GinHandler
PortsInterfaces que conectam camadasOrderRepository, 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:

  1. Go e gRPC - Comunicação entre serviços
  2. Go para Microserviços - Arquitetura distribuída
  3. Go Testing - Testes avançados
  4. Go para APIs REST - Fundamentos HTTP

Clean Architecture em Go: código limpo, testável e escalável. Compartilhe sua estrutura!