Executando verificação de segurança...
1

Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 2.3 - Unit of Work: coordenando mudanças no domínio

Capa

Na primeira parte desta série exploramos como o domínio pode ser modelado, protegido e orquestrado. Mas todo domínio, por mais elegante que seja, precisa encarar uma realidade inescapável: os dados precisam ser persistidos. Não basta calcular, validar ou expressar regras de negócio; é preciso garantir que esse estado sobreviva ao tempo. É nesse momento que a arquitetura encontra a infraestrutura, e que padrões se tornam decisivos para equilibrar clareza do modelo e eficiência técnica.

Martin Fowler dedicou uma boa parte do seu catálogo de Patterns of Enterprise Application Architecture justamente a esse tema: Active Record, Data Mapper, Unit of Work, Identity Map, Lazy Load. Cada um deles enfrenta dilemas recorrentes: como salvar e recuperar o estado do domínio sem deixá-lo refém do banco de dados? Como coordenar mudanças de forma transacional? Como manter performance sem sacrificar consistência?

Eric Evans, no Domain-Driven Design, sempre reforçou que a persistência deve ser invisível ao domínio. Para ele, o modelo precisa refletir a linguagem do negócio, não o schema do banco. Repositórios, Application Services e agregados só cumprem bem seu papel quando o banco fica em segundo plano, um detalhe de implementação.

Robert C. Martin, em Clean Architecture, é ainda mais radical: o banco de dados é um detalhe de infraestrutura, nunca o centro da arquitetura. Ele insiste que o domínio e os casos de uso não devem ter dependência alguma de frameworks ou tecnologias de armazenamento. O dado pode estar em SQL, NoSQL, arquivo em disco ou até mesmo em memória; o que importa é que o domínio continue protegido.

Nesta segunda parte da série, vamos olhar para os padrões de persistência com esses três óculos: Fowler, Evans e Uncle Bob. Vamos ver onde eles brilham, onde tropeçam e que escolhas arquiteturais nos forçam a enfrentar. E, como sempre, vamos usar exemplos em C# para tornar o debate mais concreto.


Índice da Série

Parte 1 – A lógica do domínio

Parte 2 – Persistência e Infraestrutura

Parte 3 – Apresentação e Integração


Unit of Work: coordenando mudanças no domínio

Introdução

Quando trabalhamos com persistência, não basta apenas salvar e buscar entidades de forma isolada. A realidade é que, em quase todo caso de uso, múltiplos objetos são criados, modificados ou removidos em conjunto, e essas mudanças precisam ser persistidas de maneira atômica. É nesse contexto que Martin Fowler apresenta, em Patterns of Enterprise Application Architecture, o padrão Unit of Work.

O Unit of Work funciona como um gerente de alterações: rastreia tudo o que foi criado, modificado ou removido durante uma transação de negócio e, ao final, aplica as mudanças em conjunto. Tudo ou nada. Eric Evans, no Domain-Driven Design, lembra que esse padrão é infraestrutura, não domínio. Ele apoia os Application Services e os repositórios, mas nunca deve aparecer dentro de entidades ou value objects. Já Robert C. Martin, em Clean Architecture, reforça: o domínio não deve conhecer nada de transações ou bancos. O máximo que ele enxerga é uma abstração simples de Commit() e Rollback().


Exemplo prático em C#

Vamos considerar o caso de uso “Realizar Pedido”. Um pedido confirmado gera uma fatura, consome estoque e dispara eventos de integração. Nada disso pode acontecer pela metade. Se o pedido é salvo mas o estoque não é reservado, o sistema fica inconsistente. É para resolver esse problema que Martin Fowler descreveu o padrão Unit of Work.

// Domínio: Pedido gera Invoice como invariante
public class Order
{
    private readonly List<OrderLine> _lines = new();
    public OrderId Id { get; }
    public CustomerId CustomerId { get; }
    public OrderStatus Status { get; private set; }
    public IReadOnlyList<OrderLine> Lines => _lines;
    public Invoice? Invoice { get; private set; }

    private Order(OrderId id, CustomerId customerId) 
    {
        Id = id;
        CustomerId = customerId;
        Status = OrderStatus.Draft;
    }

    public static Order Create(CustomerId customerId) 
        => new(new OrderId(Guid.NewGuid()), customerId);

    public void AddLine(ProductId productId, int quantity, Money unitPrice)
    {
        if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        _lines.Add(new OrderLine(productId, quantity, unitPrice));
    }

    public void Confirm()
    {
        if (!_lines.Any())
            throw new InvalidOperationException("Cannot confirm empty order.");
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Order already confirmed.");

        Status = OrderStatus.Confirmed;
        Invoice = new Invoice(new InvoiceId(Guid.NewGuid()), Id, Total());
    }

    public Money Total() =>
        _lines.Aggregate(new Money(0, "USD"),
            (acc, l) => acc.Add(l.UnitPrice.Multiply(l.Quantity)));
}

// Outras entidades
public record Invoice(InvoiceId Id, OrderId OrderId, Money Amount);
public record OrderLine(ProductId ProductId, int Quantity, Money UnitPrice);
public record OutboxMessage(Guid Id, string Type, string Payload, DateTime OccurredOnUtc);
public class InventoryItem
{
    public ProductId ProductId { get; }
    public int Available { get; private set; }
    public InventoryItem(ProductId productId, int available) 
    { ProductId = productId; Available = available; }

    public void Reserve(int quantity)
    {
        if (Available < quantity) throw new InvalidOperationException("Insufficient stock.");
        Available -= quantity;
    }
}

// Value Objects
public readonly record struct OrderId(Guid Value);
public readonly record struct InvoiceId(Guid Value);
public readonly record struct CustomerId(Guid Value);
public readonly record struct ProductId(Guid Value);
public readonly record struct Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Currency mismatch.");
        return new Money(Amount + other.Amount, Currency);
    }
    public Money Multiply(int factor) => new(Amount * factor, Currency);
}

// Contratos de repositórios
public interface IOrderRepository 
{ 
    void Add(Order order); 
}

public interface IInventoryRepository
{
    InventoryItem GetByProductId(ProductId id);
    void Update(InventoryItem item);
}

public interface IOutboxRepository 
{ 
    void Add(OutboxMessage message); 
}

// Unit of Work (abstração)
public interface IUnitOfWork : IDisposable
{
    void Commit();
    void Rollback();
}

// Application Service
public class PlaceOrderService
{
    private readonly IOrderRepository _orders;
    private readonly IInventoryRepository _inventory;
    private readonly IOutboxRepository _outbox;
    private readonly IUnitOfWork _uow;

    public PlaceOrderService(
        IOrderRepository orders,
        IInventoryRepository inventory,
        IOutboxRepository outbox,
        IUnitOfWork uow)
    {
        _orders = orders;
        _inventory = inventory;
        _outbox = outbox;
        _uow = uow;
    }

    public OrderId Execute(CustomerId customerId, IEnumerable<(ProductId, int, Money)> lines)
    {
        var order = Order.Create(customerId);

        foreach (var (productId, qty, price) in lines)
            order.AddLine(productId, qty, price);

        try
        {
            order.Confirm(); // Domínio gera a Invoice

            // Reserva estoque
            foreach (var line in order.Lines)
            {
                var stock = _inventory.GetByProductId(line.ProductId);
                stock.Reserve(line.Quantity);
                _inventory.Update(stock);
            }

            // Persiste pedido e invoice
            _orders.Add(order);

            // Registra evento de integração (Outbox)
            var evt = new { order.Id, order.CustomerId, Lines = order.Lines };
            var payload = System.Text.Json.JsonSerializer.Serialize(evt);
            _outbox.Add(new OutboxMessage(Guid.NewGuid(), "OrderPlaced", payload, DateTime.UtcNow));

            _uow.Commit();
            return order.Id;
        }
        catch
        {
            _uow.Rollback();
            throw;
        }
    }
}

Aqui, a Invoice nasce no domínio como uma invariante: todo pedido confirmado gera sua fatura. O serviço de aplicação apenas orquestra a transação: reserva o estoque, persiste o pedido (com a fatura embutida) e registra o evento. Tudo-or-nada, coordenado pelo Unit of Work.


O Entity Framework Core e o Unit of Work

O EF Core já implementa esse padrão em seu DbContext, que rastreia alterações e persiste tudo em lote via SaveChanges(). Mas expor diretamente o DbContext na aplicação cria acoplamento. Em linha com Evans e Uncle Bob, o ideal é encapsular o DbContext em repositórios e fornecer uma abstração de IUnitOfWork à aplicação. Assim, o Application Service fala em termos de Commit() e Rollback(), sem conhecer EF Core ou SQL.


O preço da coordenação

O Unit of Work garante consistência, mas não é gratuito: Fowler alerta para a complexidade de rastrear muitas entidades em memória. Evans reforça que é um detalhe técnico e não pode invadir o domínio. Uncle Bob lembra: se casos de uso dependem diretamente de APIs de transação específicas de banco, a arquitetura já foi comprometida.

O remédio é disciplina: manter o Unit of Work invisível ao domínio, encapsulado em uma interface mínima, servindo apenas para coordenar persistência nos Application Services.


Conclusão

O Unit of Work é o padrão que fecha o ciclo iniciado pelo Data Mapper e pelo Repository. Ele garante que múltiplas entidades e agregados sejam persistidos de forma atômica, respeitando invariantes de negócio como “pedido confirmado gera fatura”. Fowler o descreve como o gestor das alterações, Evans mostra como ele apoia a consistência sem poluir o modelo, e Uncle Bob o aceita como detalhe de infraestrutura, desde que não atravesse as fronteiras do domínio.

No próximo artigo, veremos padrões que tratam de performance e consistência em outro nível: Identity Map e Lazy Load, que ajudam a evitar duplicação de objetos e carregamento desnecessário, mas também trazem dilemas arquiteturais.

Carregando publicação patrocinada...