Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 1.1 - Transaction Script: Entre Proceduralismo, Domínio Rico e Arquitetura Limpa

Nos últimos vinte anos, três nomes se tornaram referência inevitável sempre que falamos sobre arquitetura de software: Martin Fowler, Eric Evans e Robert C. Martin. Cada um deles trouxe uma perspectiva única, mas complementar, para o ofício de projetar sistemas duradouros. Fowler, com seus padrões de aplicação empresarial, nos ofereceu um catálogo de soluções pragmáticas para problemas recorrentes. Evans, com o Domain-Driven Design, nos mostrou como aproximar o software da linguagem do negócio e como dar forma ao que realmente importa em um sistema: o domínio. E Uncle Bob, com a Clean Architecture, reforçou os princípios e as camadas que protegem esse domínio da corrosão do tempo, dos frameworks e da infraestrutura.
Esta série de artigos nasce do desejo de cruzar esses três olhares. A cada padrão descrito por Fowler, vamos buscar o eco que ele encontra em Evans e em Martin. Em alguns momentos, haverá convergência; em outros, choque. Mas em todos, a discussão nos ajuda a entender não apenas o “como” programar, mas o “porquê” de certas escolhas arquiteturais.
Vamos usar exemplos em C#, trazer referências aos livros originais e sempre terminar com uma reflexão sobre quando e por que adotar (ou evitar) cada padrão. A ideia não é canonizar um estilo, mas oferecer um mapa de raciocínio para que você, como engenheiro de software, tenha mais clareza ao tomar suas decisões.
Índice da Série
Parte 1 – A lógica do domínio
- 1.1 Transaction Script: do procedural ao domínio rico
- 1.2 Table Module: tabelas falam mais alto que objetos?
- 1.3 Domain Model: o coração da arquitetura em três vozes
- 1.4 Service Layer: orquestrando casos de uso
Parte 2 – Persistência e Infraestrutura
- 2.1 Active Record: simplicidade que pode custar caro
- 2.2 Data Mapper: separando domínio e banco de dados
- 2.3 Unit of Work: coordenando mudanças no domínio
- 2.4 Identity Map & Lazy Load: truques de performance e consistência
Parte 3 – Apresentação e Integração
- 3.1 MVC e Front Controller: a porta de entrada
- 3.2 Template View e Page Controller: quando a UI dita o jogo
- 3.3 Gateways e Mappers: defendendo o domínio
- 3.4 Mensageria e integração: eventos em três perspectivas
Transaction Script: Entre Proceduralismo, Domínio Rico e Arquitetura Limpa
Quando Martin Fowler descreve o Transaction Script em Patterns of Enterprise Application Architecture, ele não o apresenta como algo glamouroso ou altamente sofisticado. Na verdade, o padrão é quase uma confissão: "nem sempre precisamos de DDD, nem sempre precisamos de uma Onion Architecture; às vezes, o problema pede apenas uma sequência de passos organizados para atender uma requisição". É simples, direto e, por isso mesmo, útil.
Um Transaction Script nada mais é do que uma função ou método que orquestra uma transação de negócio. Imagine que você precisa calcular o total de um pedido e salvar no banco. Em vez de modelar Order, LineItem, Money e agregar invariantes complexos, você simplesmente escreve um método que percorre os itens, calcula o total e persiste. Um caso de uso, uma função.
Fowler e a simplicidade pragmática
Fowler deixa claro: este padrão é adequado quando as regras de negócio são simples ou pouco mutáveis. É uma ótima escolha para sistemas que não possuem um domínio intrincado. O benefício é a clareza e o baixo custo inicial de implementação. O problema aparece quando o sistema cresce: duplicação de lógica, falta de reutilização e dificuldade de adaptação começam a corroer o código.
Um exemplo simples em C# poderia ser:
public sealed class OrderService
{
private readonly OrderGateway _gateway;
public OrderService(OrderGateway gateway)
{
_gateway = gateway;
}
// Transaction Script: calcula total e persiste com um gateway (sem modelo rico)
public int PlaceOrder(int customerId, List<OrderItemDto> items)
{
decimal total = 0m;
foreach (var item in items)
total += item.Price * item.Quantity;
// Inserimos o cabeçalho do pedido e, em seguida, as linhas
var orderId = _gateway.InsertOrder(customerId, total, DateTime.UtcNow);
foreach (var i in items)
_gateway.InsertOrderItem(orderId, i.ProductId, i.Quantity, i.Price);
return orderId;
}
}
// DTO simples, sem comportamento de domínio
public sealed class OrderItemDto
{
public int ProductId { get; init; }
public int Quantity { get; init; }
public decimal Price { get; init; }
}
O OrderGateway encapsula acesso tabular (queries e comandos). Ele não “reconstitui” entidades ricas; apenas expõe operações para ler/gravar registros.
using System.Data;
using System.Data.Common;
public sealed class OrderGateway
{
private readonly DbConnection _conn;
public OrderGateway(DbConnection conn) => _conn = conn;
public int InsertOrder(int customerId, decimal total, DateTime createdAt)
{
EnsureOpen();
using var cmd = _conn.CreateCommand();
cmd.CommandText = @"
INSERT INTO Orders (CustomerId, Total, Status, CreatedAt)
VALUES (@cid, @total, 'Open', @createdAt);
SELECT CAST(SCOPE_IDENTITY() AS INT);";
Add(cmd, "@cid", customerId, DbType.Int32);
Add(cmd, "@total", total, DbType.Decimal, precision: 18, scale: 2);
Add(cmd, "@createdAt", createdAt, DbType.DateTime2);
var id = (int)(cmd.ExecuteScalar() ?? 0);
return id;
}
public void InsertOrderItem(int orderId, int productId, int qty, decimal unitPrice)
{
EnsureOpen();
using var cmd = _conn.CreateCommand();
cmd.CommandText = @"
INSERT INTO OrderItems (OrderId, ProductId, Quantity, UnitPrice)
VALUES (@oid, @pid, @qty, @price);";
Add(cmd, "@oid", orderId, DbType.Int32);
Add(cmd, "@pid", productId, DbType.Int32);
Add(cmd, "@qty", qty, DbType.Int32);
Add(cmd, "@price", unitPrice, DbType.Decimal, precision: 18, scale: 2);
cmd.ExecuteNonQuery();
}
private void EnsureOpen()
{
if (_conn.State != ConnectionState.Open)
_conn.Open();
}
private static void Add(DbCommand cmd, string name, object value, DbType type,
int? size = null, byte? precision = null, byte? scale = null)
{
var p = cmd.CreateParameter();
p.ParameterName = name;
p.DbType = type;
if (size.HasValue) p.Size = size.Value;
if (p is IDbDataParameter dp)
{
if (precision.HasValue) dp.Precision = precision.Value;
if (scale.HasValue) dp.Scale = scale.Value;
}
p.Value = value ?? DBNull.Value;
cmd.Parameters.Add(p);
}
}
Perceba que a lógica fica no método do serviço (somar Price * Quantity) e a persistência fica no gateway. Não há entities/aggregates com comportamento; há dados e passos, como Fowler descreve.
Eric Evans e a crítica ao proceduralismo
Quando Eric Evans escreve Domain-Driven Design, ele praticamente inicia uma cruzada contra esse estilo de modelagem. Para Evans, um Transaction Script é a antítese de um Modelo de Domínio Rico. Ao colocar toda a lógica em scripts transacionais, perde-se a oportunidade de criar um vocabulário ubíquo, de aproximar código e negócio.
Se um sistema é complexo, insistir nesse padrão leva ao que Evans chama de anemic domain model: objetos que não passam de estruturas de dados, enquanto a inteligência fica espalhada em services ou métodos utilitários. Para ele, se há complexidade, devemos investir em Entidades com comportamento, Agregados que garantem invariantes e Serviços de domínio que preservam o sentido do negócio.
No nosso exemplo de pedidos, Evans defenderia que o cálculo do total deveria estar dentro da entidade Order, e não num service transacional:
public class Order
{
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public decimal Total => _items.Sum(i => i.Subtotal);
public void AddItem(int productId, int quantity, decimal price)
{
_items.Add(new OrderItem(productId, quantity, price));
}
}
public class OrderItem
{
public int ProductId { get; }
public int Quantity { get; }
public decimal Price { get; }
public decimal Subtotal => Quantity * Price;
public OrderItem(int productId, int quantity, decimal price)
{
ProductId = productId;
Quantity = quantity;
Price = price;
}
}
Aqui, o domínio "fala por si". Não precisamos calcular o total em cada transação; o próprio Order garante a lógica.
Uncle Bob e a Arquitetura Limpa
Robert C. Martin, em Clean Architecture, ficaria no meio do caminho. Ele não condenaria de forma tão dura o Transaction Script em si, mas lembraria que concentrar regras em métodos procedurais viola princípios como o SRP (Single Responsibility Principle) e o OCP (Open-Closed Principle). Para ele, a questão não é apenas sobre estilo de modelagem, mas sobre dependências.
Em um Transaction Script típico, os métodos conhecem detalhes de persistência, cálculos e até formatação. Isso mistura níveis de abstração e torna o código frágil. Uncle Bob insistiria: separe regras de negócio de detalhes de infraestrutura. No limite, é possível usar um Transaction Script dentro de uma Use Case Interactor, mas ainda assim isolando persistência e UI em camadas externas.
O mesmo exemplo, se reescrito com um toque de Clean Architecture, teria um Use Case intermediário:
public class PlaceOrderUseCase
{
private readonly IOrderRepository _repository;
public PlaceOrderUseCase(IOrderRepository repository)
{
_repository = repository;
}
public void Execute(PlaceOrderRequest request)
{
var order = new Order();
foreach (var item in request.Items)
{
order.AddItem(item.ProductId, item.Quantity, item.Price);
}
_repository.Save(order);
}
}
Note: o cálculo já está encapsulado no domínio (Order), e a orquestração (Transaction Script) se transforma num caso de uso limpo, sem dependência direta de banco de dados ou frameworks.
Conclusão
O Transaction Script é como aquele martelo pequeno que todo mundo tem na caixa de ferramentas: prático, barato e útil para pequenos reparos. Fowler o descreve como uma solução simples para problemas simples. Evans nos alerta de que confiar nele em sistemas complexos é plantar as sementes de um futuro doloroso. Já Robert C. Martin reforça a importância de não misturar responsabilidades e de proteger o coração do sistema da corrosão dos detalhes.
A verdade é que os três não se contradizem: eles apenas enxergam o mesmo padrão em estágios diferentes da vida de um software. Se o sistema é pequeno, Transaction Script é pragmático. Se cresce em complexidade, o DDD de Evans se torna essencial. E, em qualquer cenário, os princípios de arquitetura limpa de Martin devem ser nosso norte, garantindo que a simplicidade não descambe em bagunça.
No próximo artigo da série, vamos analisar o Table Module, outro padrão descrito por Fowler, e entender por que ele soa tão natural em sistemas orientados a dados e ao mesmo tempo conflita com a filosofia de DDD e Clean Architecture.