<h2>Domain-Driven Design: Fundamentos e Aplicação em Go</h2>
<p>Domain-Driven Design (DDD) é uma abordagem arquitetural que coloca o domínio do negócio no centro do desenvolvimento. Diferentemente de arquiteturas orientadas apenas por frameworks ou tecnologia, DDD força o desenvolvedor a entender profundamente as regras de negócio e traduzi-las em código estruturado. Em Go, essa prática ganha poder especial devido à simplicidade da linguagem e sua forte tipagem.</p>
<p>O propósito deste artigo é capacitá-lo a implementar os três pilares fundamentais de DDD em Go: Entidades, Value Objects e Repositórios. Esses componentes formam a espinha dorsal de qualquer arquitetura de domínio bem estruturada. Não trataremos apenas de padrões genéricos, mas de como aplicá-los de forma prática e funcional, respeitando tanto as convenções de DDD quanto as idiossincrasias da linguagem Go.</p>
<h3>Por que DDD importa em Go</h3>
<p>Go foi projetada para ser pragmática e direta. Sua falta de herança, sua abordagem funcional para composição e seu sistema de interfaces implícitas criam um ambiente natural para DDD. Você não terá que lutar contra a linguagem para implementar conceitos de domínio — pelo contrário, Go simplifica muitos padrões complexos que em outras linguagens exigem boilerplate considerável.</p>
<h2>Entidades: O Coração do Domínio</h2>
<p>Uma Entidade é um objeto com identidade única que persiste ao longo do tempo. Diferentemente de outros objetos, duas entidades são consideradas diferentes se suas identidades são diferentes, mesmo que todos os seus atributos sejam idênticos. Se você tem dois usuários com o mesmo nome e email, eles ainda são usuários distintos porque possuem IDs diferentes.</p>
<h3>Estrutura de uma Entidade</h3>
<p>No Go, uma entidade é representada como uma <code>struct</code> que encapsula tanto dados quanto comportamento relacionado ao conceito de negócio. A identidade deve ser imutável após a criação. Veja um exemplo prático:</p>
<pre><code class="language-go">package domain
import (
"errors"
"time"
)
// User é uma entidade que representa um usuário no domínio
type User struct {
id string
name string
email string
status string
createdAt time.Time
}
// NewUser é um construtor que garante invariantes de negócio
func NewUser(id, name, email string) (*User, error) {
if id == "" {
return nil, errors.New("id cannot be empty")
}
if name == "" {
return nil, errors.New("name cannot be empty")
}
if !isValidEmail(email) {
return nil, errors.New("email format is invalid")
}
return &User{
id: id,
name: name,
email: email,
status: "active",
createdAt: time.Now(),
}, nil
}
// ID retorna a identidade única do usuário
func (u *User) ID() string {
return u.id
}
// Email retorna o email do usuário
func (u *User) Email() string {
return u.email
}
// Name retorna o nome do usuário
func (u *User) Name() string {
return u.name
}
// Deactivate aplica uma regra de negócio: desativar um usuário
func (u *User) Deactivate() error {
if u.status == "deactivated" {
return errors.New("user is already deactivated")
}
u.status = "deactivated"
return nil
}
// IsActive verifica se o usuário está ativo
func (u *User) IsActive() bool {
return u.status == "active"
}
// isValidEmail valida o formato de email (simplificado)
func isValidEmail(email string) bool {
return len(email) > 0 && len(email) < 255
}</code></pre>
<p>Observe que os campos da entidade são privados (começam com letra minúscula). Isso força o consumidor da entidade a usar métodos para acessar seus dados, permitindo que você implemente lógica de validação e regras de negócio. O construtor <code>NewUser</code> não apenas cria a instância — ele valida invariantes do domínio. Se qualquer invariante for violado, retorna um erro.</p>
<h3>Comportamento dentro da Entidade</h3>
<p>As entidades devem encapsular não apenas estado, mas comportamento que modifica esse estado. O método <code>Deactivate()</code> exemplifica isso: a regra de negócio "um usuário não pode ser desativado duas vezes" está implementada como uma verificação dentro da entidade, não no código cliente. Isso significa que é impossível violar essa regra usando a API corretamente.</p>
<h2>Value Objects: Imutabilidade e Identidade por Valor</h2>
<p>Um Value Object é um objeto sem identidade própria, cuja igualdade é baseada exclusivamente no valor de seus atributos. Se você possui dois Value Objects com os mesmos dados, eles são considerados iguais. Diferentemente das entidades, Value Objects são imutáveis e descartáveis — você não os persiste por referência, mas por valor.</p>
<h3>Estrutura de um Value Object</h3>
<p>Value Objects são perfeitos para representar conceitos de domínio que não precisam de rastreamento individual. Emails, endereços, valores monetários são excelentes candidatos:</p>
<pre><code class="language-go">package domain
import (
"errors"
"fmt"
"strings"
)
// Email é um Value Object que encapsula a lógica de validação de email
type Email struct {
value string
}
// NewEmail cria um novo Email com validação
func NewEmail(email string) (*Email, error) {
email = strings.TrimSpace(email)
if !isValidEmailFormat(email) {
return nil, errors.New("invalid email format")
}
return &Email{value: email}, nil
}
// String retorna o valor do email
func (e *Email) String() string {
return e.value
}
// Equals compara dois Value Objects Email
func (e Email) Equals(other Email) bool {
if other == nil {
return false
}
return e.value == other.value
}
// isValidEmailFormat é uma validação simplificada
func isValidEmailFormat(email string) bool {
return len(email) > 0 && strings.Contains(email, "@")
}
// Money é um Value Object que representa quantidade monetária
type Money struct {
amount float64
currency string
}
// NewMoney cria um novo Money
func NewMoney(amount float64, currency string) (*Money, error) {
if amount < 0 {
return nil, errors.New("amount cannot be negative")
}
if currency == "" {
return nil, errors.New("currency cannot be empty")
}
return &Money{amount: amount, currency: currency}, nil
}
// Amount retorna o valor monetário
func (m *Money) Amount() float64 {
return m.amount
}
// Currency retorna a moeda
func (m *Money) Currency() string {
return m.currency
}
// Add realiza adição entre Money objects
func (m Money) Add(other Money) (*Money, error) {
if m.currency != other.currency {
return nil, errors.New("cannot add money with different currencies")
}
return NewMoney(m.amount+other.amount, m.currency)
}
// Equals compara dois Money objects
func (m Money) Equals(other Money) bool {
if other == nil {
return false
}
return m.amount == other.amount && m.currency == other.currency
}
// String implementa Stringer para Money
func (m *Money) String() string {
return fmt.Sprintf("%.2f %s", m.amount, m.currency)
}</code></pre>
<p>A imutabilidade é crucial. Uma vez criado, um Value Object nunca deve mudar. Se você precisa "modificar" um Value Object, cria um novo. Isso torna Value Objects completamente seguros para compartilhar entre goroutines sem locks.</p>
<h3>Quando usar Value Objects</h3>
<p>Use Value Objects para conceitos do domínio que são claramente valores, não entidades. Preço, coordenada geográfica, intervalo de data, status com número limitado de opções — tudo isso é melhor representado como Value Object. A vantagem é que você coloca lógica de validação e comportamento onde ela pertence: na definição do conceito.</p>
<h2>Repositórios: Abstração do Armazenamento</h2>
<p>Um Repositório é uma abstração que simula uma coleção em memória de agregados (entidades e seus Value Objects relacionados). O repositório isola a lógica de persistência do domínio, permitindo que você teste a lógica de negócio sem depender de um banco de dados real.</p>
<h3>Interface e Implementação do Repositório</h3>
<p>O padrão fundamental é definir uma interface no domínio e implementações concretas na camada de infraestrutura:</p>
<pre><code class="language-go">package domain
// UserRepository define a interface para persistência de usuários
type UserRepository interface {
// Save armazena ou atualiza um usuário
Save(user *User) error
// FindByID recupera um usuário pelo ID
FindByID(id string) (*User, error)
// FindByEmail recupera um usuário pelo email
FindByEmail(email string) (*User, error)
// Delete remove um usuário
Delete(id string) error
// FindAll retorna todos os usuários
FindAll() ([]*User, error)
}</code></pre>
<p>Agora, uma implementação em memória para testes:</p>
<pre><code class="language-go">package infrastructure
import (
"errors"
"sync"
"myapp/domain"
)
// InMemoryUserRepository é uma implementação em memória
type InMemoryUserRepository struct {
mu sync.RWMutex
users map[string]*domain.User
}
// NewInMemoryUserRepository cria um novo repositório em memória
func NewInMemoryUserRepository() *InMemoryUserRepository {
return &InMemoryUserRepository{
users: make(map[string]*domain.User),
}
}
// Save armazena ou atualiza um usuário
func (r InMemoryUserRepository) Save(user domain.User) error {
if user == nil {
return errors.New("user cannot be nil")
}
r.mu.Lock()
defer r.mu.Unlock()
r.users[user.ID()] = user
return nil
}
// FindByID recupera um usuário pelo ID
func (r InMemoryUserRepository) FindByID(id string) (domain.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, exists := r.users[id]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
// FindByEmail recupera um usuário pelo email
func (r InMemoryUserRepository) FindByEmail(email string) (domain.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, user := range r.users {
if user.Email() == email {
return user, nil
}
}
return nil, errors.New("user not found")
}
// Delete remove um usuário
func (r *InMemoryUserRepository) Delete(id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.users[id]; !exists {
return errors.New("user not found")
}
delete(r.users, id)
return nil
}
// FindAll retorna todos os usuários
func (r InMemoryUserRepository) FindAll() ([]domain.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*domain.User, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
return users, nil
}</code></pre>
<h3>Implementação com Banco de Dados Real</h3>
<p>Aqui está uma implementação com PostgreSQL usando a biblioteca <code>pgx</code>:</p>
<pre><code class="language-go">package infrastructure
import (
"context"
"errors"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"myapp/domain"
)
// PostgresUserRepository implementa UserRepository com PostgreSQL
type PostgresUserRepository struct {
pool *pgxpool.Pool
}
// NewPostgresUserRepository cria um novo repositório PostgreSQL
func NewPostgresUserRepository(pool pgxpool.Pool) PostgresUserRepository {
return &PostgresUserRepository{pool: pool}
}
// Save armazena ou atualiza um usuário
func (r PostgresUserRepository) Save(user domain.User) error {
query := `
INSERT INTO users (id, name, email, status, created_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
status = EXCLUDED.status
`
_, err := r.pool.Exec(
context.Background(),
query,
user.ID(),
user.Name(),
user.Email(),
"active", // simplificado, deveria expor Status
user.CreatedAt(), // assumindo que existe esse getter
)
return err
}
// FindByID recupera um usuário pelo ID
func (r PostgresUserRepository) FindByID(id string) (domain.User, error) {
query := SELECT id, name, email FROM users WHERE id = $1
var userID, name, email string
err := r.pool.QueryRow(context.Background(), query, id).
Scan(&userID, &name, &email)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.New("user not found")
}
return nil, err
}
return domain.NewUser(userID, name, email)
}
// FindByEmail recupera um usuário pelo email
func (r PostgresUserRepository) FindByEmail(email string) (domain.User, error) {
query := SELECT id, name, email FROM users WHERE email = $1
var userID, userName, userEmail string
err := r.pool.QueryRow(context.Background(), query, email).
Scan(&userID, &userName, &userEmail)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.New("user not found")
}
return nil, err
}
return domain.NewUser(userID, userName, userEmail)
}
// Delete remove um usuário
func (r *PostgresUserRepository) Delete(id string) error {
result, err := r.pool.Exec(context.Background(), DELETE FROM users WHERE id = $1, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return errors.New("user not found")
}
return nil
}
// FindAll retorna todos os usuários
func (r PostgresUserRepository) FindAll() ([]domain.User, error) {
query := SELECT id, name, email FROM users
rows, err := r.pool.Query(context.Background(), query)
if err != nil {
return nil, err
}
defer rows.Close()
users := make([]*domain.User, 0)
for rows.Next() {
var userID, name, email string
err := rows.Scan(&userID, &name, &email)
if err != nil {
return nil, err
}
user, err := domain.NewUser(userID, name, email)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, rows.Err()
}</code></pre>
<h3>Casos de Uso e Injeção de Dependência</h3>
<p>Agora vemos como tudo funciona junto em um caso de uso:</p>
<pre><code class="language-go">package application
import "myapp/domain"
// CreateUserUseCase encapsula a lógica de criação de usuário
type CreateUserUseCase struct {
userRepository domain.UserRepository
}
// NewCreateUserUseCase cria um novo caso de uso
func NewCreateUserUseCase(repo domain.UserRepository) *CreateUserUseCase {
return &CreateUserUseCase{userRepository: repo}
}
// Execute executa a criação do usuário
func (uc *CreateUserUseCase) Execute(id, name, email string) error {
// Validar se o email já existe
_, err := uc.userRepository.FindByEmail(email)
if err == nil {
return domain.NewDomainError("email already registered")
}
// Criar a entidade (validações ocorrem aqui)
user, err := domain.NewUser(id, name, email)
if err != nil {
return err
}
// Persistir
return uc.userRepository.Save(user)
}</code></pre>
<p>Graças à injeção de dependência via interface, este caso de uso funciona exatamente igual com <code>InMemoryUserRepository</code> ou <code>PostgresUserRepository</code>. Você pode testá-lo sem banco de dados real, e mudar de banco de dados sem alterar uma única linha aqui.</p>
<h2>Conclusão</h2>
<p>Os três pilares de Domain-Driven Design em Go — Entidades, Value Objects e Repositórios — trabalham em conjunto para criar código que espelha o domínio do negócio. Entidades encapsulam identidade e comportamento que modifica o estado do domínio. Value Objects garantem imutabilidade e trazem validação para conceitos de valor. Repositórios abstraem a persistência, permitindo que a lógica de negócio seja testável e desacoplada de tecnologias específicas.</p>
<p>A força principal dessa abordagem em Go é sua simplicidade: sem herança complexa ou frameworks prescritivos, você fica livre para focar no que importa — modelar o domínio com precisão. As interfaces implícitas do Go favorecem naturalmente o padrão de repositório e facilitam testes. Quando aplicado corretamente, DDD em Go resulta em código que é mais fácil de entender, manter e evoluir conforme o domínio muda.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.domainlanguage.com/ddd/" target="_blank" rel="noopener noreferrer">Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice" target="_blank" rel="noopener noreferrer">Microsoft - Domain-Driven Design Learning Path</a></li>
<li><a href="https://vaughnvernon.com/" target="_blank" rel="noopener noreferrer">Implementing Domain-Driven Design - Vaughn Vernon</a></li>
<li><a href="https://golang.org/doc/effective_go" target="_blank" rel="noopener noreferrer">Go Documentation - Effective Go</a></li>
<li><a href="https://github.com/jackc/pgx" target="_blank" rel="noopener noreferrer">PostgreSQL pgx Driver Documentation</a></li>
</ul>
<p><!-- FIM --></p>