Segunda-feira de manhã. Chega a demanda urgente:
“Precisamos de uma feature nova! Clientes VIP agora têm frete grátis em compras acima de R$ 50,00. Para os demais, continua sendo R$ 100,00.”
Vamos analisar a Mentalidade de Tarefa (focada em apenas “fazer funcionar” para fechar o ticket) e a Mentalidade de Design (focada em como essa nova regra impacta a saúde do sistema a longo prazo) para resolver esse mesmo problema.
O Cenário Atual#
Lembrando:
“Precisamos de uma feature nova! Clientes VIP agora têm frete grátis em compras acima de R$ 50,00. Para os demais, continua sendo R$ 100,00.”
Temos um OrderService com a regra de frete grátis misturada com outras regras de negócio ou regras de infra, tudo dentro de um método transacional complexo:
@Service
@AllArgsConstructor
@Transactional
public class OrderService {
private static final Logger log =
LoggerFactory.getLogger(OrderService.class);
private final ProductRepository productRepository;
private final OrderRepository orderRepository;
private final InventoryEventPublisher eventPublisher;
public OrderResponse placeOrder(OrderRequest request) {
Order order = new Order();
order.setOrderNumber(UUID.randomUUID()
.toString());
order.setCustomerEmail(request.customerEmail());
order.setOrderDate(LocalDateTime.now());
order.setStatus(OrderStatus.PENDING);
BigDecimal totalAmount = BigDecimal.ZERO;
for (var itemRequest : request.items()) {
Product product =
productRepository
.findById(itemRequest.productId())
.orElseThrow(
() ->
new BusinessRuleException(
"Product not found " + "with id: " + itemRequest.productId()));
product.validateStockQuantity(itemRequest.quantity());
int quantity = itemRequest.quantity();
BigDecimal unitPrice = product.getPrice();
OrderItem orderItem = new OrderItem();
orderItem.setQuantity(quantity);
orderItem.setUnitPrice(unitPrice);
order.addItem(orderItem);
totalAmount =
totalAmount.add(unitPrice.multiply(BigDecimal.valueOf(quantity)));
int newStock = product.getStockQuantity() - quantity;
product.setStockQuantity(newStock);
productRepository.save(product);
eventPublisher.publishStockUpdated(new StockUpdate(product.getId(), newStock));
}
// Free shipping
if (totalAmount.compareTo(new BigDecimal("100")) > 0) {
order.setFreeShipping(true);
log.info("Order {} qualifies for free shipping.",
order.getOrderNumber());
}
// Manual review
if (totalAmount.compareTo(new BigDecimal("500")) > 0) {
order.setManualReview(true);
log.warn("Order {} requires manual review.",
order.getOrderNumber());
}
order.setTotalAmount(totalAmount);
Order savedOrder = orderRepository.save(order);
log.info("Order {} placed successfully.",
savedOrder.getOrderNumber());
eventPublisher.publishOrderPlaced(savedOrder);
return savedOrder.toOrderResponse();
}
}
E o OrderRequest é um record simples:
public record OrderRequest(
@NotEmpty @Email String customerEmail,
@NotEmpty @Valid List<OrderItemRequest> items) {
}
⚡ A Mentalidade de Tarefa#
Nessa abordagem, o foco é um só: remover o ticket da frente. É o caminho da menor resistência, onde o desenvolvedor age de forma reativa ao requisito. O “meu eu do passado” (ou qualquer um de nós sob pressão) pensaria: “Isso é fácil. Só preciso passar um boolean isVip e mudar o if.”
A Mudança no Request#
A alteração no OrderRequest é feita adicionando o campo da forma mais direta:
public record OrderRequestFastImpl(
@NotEmpty @Email String customerEmail,
@NotEmpty @Valid List<OrderItemRequest> items,
boolean isVip) {
}
A Lógica no Serviço#
E no serviço OrderService, a mudança é injetada no meio do método transacional:
var freeShippingLimit = request.isVip() ? new BigDecimal("50") :
new BigDecimal("100");
// Free shipping
if (totalAmount.compareTo(freeShippingLimit) > 0) {
order.setFreeShipping(true);
log.info("Order {} qualifies for free shipping.",
order.getOrderNumber());
}
As Consequências Ocultas#
A feature é entregue em 5 minutos. Mas a que custo?
Quebra de Contrato: Ao adicionar
boolean isVipno construtor do record, quebramos todos os testes e classes que instanciavamOrderRequest. É o famoso “efeito cascata” que obriga a corrigir arquivos que nem deveriam ser tocados.Adeus, Testes Unitários: Para testar se essa regra do VIP funciona, precisaríamos instanciar o
OrderService. Mas como ele depende de repositórios e publishers, teríamos que “mocar” o universo inteiro só para validar umif. O resultado é oOrderServiceTest, um teste de “alto custo” para escrever e manter:
@Test
void tooExpensiveToWrite() {
// Criando os mocks...
OrderRepository orderRepository = Mockito.mock(OrderRepository.class);
ProductRepository productRepository = Mockito.mock(ProductRepository.class);
InventoryEventPublisher eventPublisher = Mockito.mock(InventoryEventPublisher.class);
// ...Configurando os mocks
OrderServiceFastImpl orderService = new OrderServiceFastImpl(productRepository, orderRepository, eventPublisher);
OrderRequestFastImpl orderRequest = new OrderRequestFastImpl("teste@gmail.com", null, false);
// Para finalmente começar a usar o método...
orderService.placeOrder(orderRequest);
}
}
- Armadilha do Primitivo: Usei
booleanprimitivo. Se o front-end mandar um JSON sem o campoisVip, ele assumefalseautomaticamente. Nunca saberei se o cliente não é VIP ou se houve um erro de integração. Imagine o cenário abaixo, onde o App foi atualizado mas a Web não:
graph TD
A[Smartphone App v2.0] -- "Envia {..., isVip: true}" --> B(Backend)
C[Web Client v1.5] -- "Envia {...} (isVip ausente)" --> B
B -- "isVip assume false (Primitivo)" --> D{Regra de VIP não aplicada}
style A fill:#d4edda,stroke:#28a745
style C fill:#f8d7da,stroke:#dc3545
- Complexidade: Adicionei mais lógica condicional dentro de um método que já fazia muita coisa.
🏗️ A Mentalidade de Design#
Aqui, aplicamos o Lema do Escoteiro: deixar a área mais limpa do que a encontramos. O foco não é apenas “fazer funcionar”, mas garantir que o sistema continue evoluindo sem medo. No artigo anterior vimos como identificar responsabilidades, e aqui aplicamos isso na prática.
1. Analisando o Legado (Onde está a regra?)#
Antes de sair codando, analisamos o código original para identificar se conseguimos extrair algum conceito de negócio que está “escondido” ou misturado com outras regras de negócio ou regras de infra. No nosso caso:
// Free shipping
if (totalAmount.compareTo(new BigDecimal("100")) > 0) {
order.setFreeShipping(true);
log.info("Order {} qualifies for free shipping.",
order.getOrderNumber());
}
Nesta mentalidade, percebemos que é perfeitamente possível extrair essa lógica! Ela depende unicamente do valor total e do booleano isVip.
Portanto, antes de modificar qualquer coisa no serviço, criamos uma classe que representa essa Regra de Negócio isolada.
2. Começando pelo Teste (O Contrato da Regra)#
Com o conceito identificado, utilizamos o TDD (Test Driven Development). Antes de existir a lógica, definimos como ela deve se comportar. O ShippingPolicyTest valida os cenários de VIP, cliente padrão e retrocompatibilidade (nulos):
class ShippingPolicyTest {
@Test
@DisplayName("VIPs should get free shipping for orders above 50.00")
void vipShouldHaveLowerLimit() {
// Given
var isVip = true;
// When / Then
assertThat(applyFreeShipping(amount("50.01"), isVip)).isTrue();
assertThat(applyFreeShipping(amount("50.00"), isVip)).isFalse();
}
@Test
@DisplayName("Standard customers should get free shipping for orders above 100.00")
void standardShouldHaveHigherLimit() {
// Given
var isVip = false;
// When / Then
assertThat(applyFreeShipping(amount("100.01"), isVip)).isTrue();
assertThat(applyFreeShipping(amount("100.00"), isVip)).isFalse();
}
@Test
@DisplayName("Null VIP status is treated as Standard customer")
void nullVipShouldBeTreatedAsStandard() {
// Given
Boolean isVip = null;
// When / Then
assertThat(applyFreeShipping(amount("100.01"), isVip)).isTrue();
assertThat(applyFreeShipping(amount("100.00"), isVip)).isFalse();
}
private BigDecimal amount(String value) {
return new BigDecimal(value);
}
}
3. Implementando e Refatorando (ShippingPolicy)#
Com os testes falhando, implementamos a ShippingPolicy. O fato de ter um teste unitário nos dá segurança para refatorar e isolar a regra em uma classe pura:
public class ShippingPolicy {
// Valores nomeados conforme a regra de negócio
private static final BigDecimal STANDARD_LIMIT = new BigDecimal("100" +
".00");
private static final BigDecimal VIP_LIMIT = new BigDecimal("50.00");
// Recebe Boolean (wrapper) para suportar nulos de clientes legados
public static boolean applyFreeShipping(BigDecimal amount,
Boolean isVip) {
// Design Defensivo: Trata null como false (Retrocompatibilidade)
BigDecimal limit = Boolean.TRUE.equals(isVip) ? VIP_LIMIT :
STANDARD_LIMIT;
return amount.compareTo(limit) > 0;
}
}
4. Protegendo o Contrato (OrderRequest)#
No OrderRequest, usamos Boolean (objeto) e mantemos um construtor compatível com a versão anterior. Ninguém quebra!
public record OrderRequestWellThought(
@NotEmpty @Email String customerEmail,
@NotEmpty @Valid List<OrderItemRequest> items,
Boolean isVip) {
// Mantendo API antiga
public OrderRequestWellThought(@NotEmpty @Email String customerEmail,
@NotEmpty @Valid List<OrderItemRequest> items) {
this(customerEmail, items, false);
}
}
5. Integração Limpa#
No OrderService, a mudança final é sutil, declarativa e não polui o método principal:
if (ShippingPolicy.applyFreeShipping(totalAmount, request.isVip())) {
order.setFreeShipping(true);
log.info("Order {} qualifies for free shipping.",
order.getOrderNumber());
Conclusão#
A Mentalidade de Tarefa resolve o problema em 5 minutos, mas cria dívida técnica, quebra clientes existentes e desencoraja testes. É uma vitória de curto prazo que pode gerar um custo desnecessário de manutenção.
A Mentalidade de Design:
- Isola a complexidade (
ShippingPolicy). - Permite testes baratos e rápidos (
ShippingPolicyTest). - Mantém compatibilidade (Construtores e Wrappers).
- Documenta o código (Nomes explícitos de constantes e classes).
Seu “Eu do Futuro” (e toda a sua equipe) vai agradecer!
E você? Tem aprendido e conseguido praticar as boas práticas de programação mesmo em código legado?
Tomara que tenha encorajado a você tentar escrever um código melhor independente da situação!
📚 Referências#
🥒🥒 Até o próximo artigo! 🥒🥒


