Go

Dominando Domain-Driven Design em Go: Entidades, Value Objects e Repositórios em Projetos Reais

15 min de leitura

Dominando Domain-Driven Design em Go: Entidades, Value Objects e Repositórios em Projetos Reais

Domain-Driven Design: Fundamentos e Aplicação em Go 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. 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. Por que DDD importa em Go 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

<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 (

&quot;errors&quot;

&quot;time&quot;

)

// 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 == &quot;&quot; {

return nil, errors.New(&quot;id cannot be empty&quot;)

}

if name == &quot;&quot; {

return nil, errors.New(&quot;name cannot be empty&quot;)

}

if !isValidEmail(email) {

return nil, errors.New(&quot;email format is invalid&quot;)

}

return &amp;User{

id: id,

name: name,

email: email,

status: &quot;active&quot;,

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 == &quot;deactivated&quot; {

return errors.New(&quot;user is already deactivated&quot;)

}

u.status = &quot;deactivated&quot;

return nil

}

// IsActive verifica se o usuário está ativo

func (u *User) IsActive() bool {

return u.status == &quot;active&quot;

}

// isValidEmail valida o formato de email (simplificado)

func isValidEmail(email string) bool {

return len(email) &gt; 0 &amp;&amp; len(email) &lt; 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 &quot;um usuário não pode ser desativado duas vezes&quot; 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 (

&quot;errors&quot;

&quot;fmt&quot;

&quot;strings&quot;

)

// 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(&quot;invalid email format&quot;)

}

return &amp;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) &gt; 0 &amp;&amp; strings.Contains(email, &quot;@&quot;)

}

// 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 &lt; 0 {

return nil, errors.New(&quot;amount cannot be negative&quot;)

}

if currency == &quot;&quot; {

return nil, errors.New(&quot;currency cannot be empty&quot;)

}

return &amp;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(&quot;cannot add money with different currencies&quot;)

}

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 &amp;&amp; m.currency == other.currency

}

// String implementa Stringer para Money

func (m *Money) String() string {

return fmt.Sprintf(&quot;%.2f %s&quot;, m.amount, m.currency)

}</code></pre>

<p>A imutabilidade é crucial. Uma vez criado, um Value Object nunca deve mudar. Se você precisa &quot;modificar&quot; 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 (

&quot;errors&quot;

&quot;sync&quot;

&quot;myapp/domain&quot;

)

// 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 &amp;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(&quot;user cannot be nil&quot;)

}

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(&quot;user not found&quot;)

}

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(&quot;user not found&quot;)

}

// 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(&quot;user not found&quot;)

}

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 (

&quot;context&quot;

&quot;errors&quot;

&quot;github.com/jackc/pgx/v4&quot;

&quot;github.com/jackc/pgx/v4/pgxpool&quot;

&quot;myapp/domain&quot;

)

// 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 &amp;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(),

&quot;active&quot;, // 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(&amp;userID, &amp;name, &amp;email)

if err != nil {

if errors.Is(err, pgx.ErrNoRows) {

return nil, errors.New(&quot;user not found&quot;)

}

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(&amp;userID, &amp;userName, &amp;userEmail)

if err != nil {

if errors.Is(err, pgx.ErrNoRows) {

return nil, errors.New(&quot;user not found&quot;)

}

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(&quot;user not found&quot;)

}

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(&amp;userID, &amp;name, &amp;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 &quot;myapp/domain&quot;

// 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 &amp;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(&quot;email already registered&quot;)

}

// 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

defer, panic e recover em Go: Controle de Fluxo Excepcional na Prática
defer, panic e recover em Go: Controle de Fluxo Excepcional na Prática

Entendendo o Fluxo Excepcional em Go Go é uma linguagem que não segue o model...

O que Todo Dev Deve Saber sobre Pacote os e filepath em Go: Sistema de Arquivos e Ambiente
O que Todo Dev Deve Saber sobre Pacote os e filepath em Go: Sistema de Arquivos e Ambiente

Entendendo o Pacote em Go O pacote é fundamental para qualquer programa Go qu...

Dominando Arrays, Slices e Maps em Go: A Base das Coleções em Projetos Reais
Dominando Arrays, Slices e Maps em Go: A Base das Coleções em Projetos Reais

Arrays: A Estrutura Fixa de Dados Arrays são coleções de tamanho fixo que arm...