No Spring, @Profile não é organização de código
@Profile costuma dar uma sensação perigosa de elegância.
Em vez de colocar um if, uma estratégia explícita ou uma decisão de domínio no fluxo da aplicação, alguém separa implementações por profile e sente que o código ficou mais "limpo".
Na superfície, parece organização.
Na prática, muitas vezes só desloca a complexidade para o bootstrap.
E isso muda bastante a leitura correta do sistema.
@Profile não foi feito para modelar regra de negócio.
Ele foi feito para decidir em que contexto de execução determinado Bean deve existir.
@Profile escolhe a aplicação que sobe, não o caminho de código
Esse é o primeiro ponto que vale fixar.
Profile é decisão de ativação do container.
Ele diz quais Beans entram ou ficam de fora quando a aplicação sobe.
Ou seja: ele não troca comportamento no meio do fluxo. Ele troca o conjunto de objetos disponíveis desde o início.
Quando você usa @Profile para separar variantes funcionais da aplicação, está dizendo que cada profile representa uma aplicação diferente.
Veja este exemplo:
public interface CalculadoraPreco {
BigDecimal calcular(Pedido pedido);
}
@Service
@Profile("varejo")
public class CalculadoraPrecoVarejo implements CalculadoraPreco {
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getSubtotal().multiply(new BigDecimal("1.10"));
}
}
@Service
@Profile("atacado")
public class CalculadoraPrecoAtacado implements CalculadoraPreco {
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getSubtotal().multiply(new BigDecimal("0.92"));
}
}
Se a regra de preço depende do tipo de pedido, do cliente ou do canal de venda, isso não é contexto de ambiente.
Isso é comportamento da aplicação.
O problema fica claro rápido:
- com profile
varejo, a aplicação inteira sobe com uma lógica; - com profile
atacado, ela inteira sobe com outra; - não existe espaço natural para os dois cenários conviverem no mesmo runtime.
Se o negócio real suporta ambos, então @Profile não organizou o código.
Ele amputou uma dimensão do domínio e a empurrou para a configuração de inicialização.
O custo real é esconder decisão de negócio no lugar errado
Quando o time olha para o código acima, parece que a seleção de comportamento ficou sofisticada.
Mas a decisão deixou de estar visível no fluxo da aplicação.
Ela foi parar em spring.profiles.active, variável de ambiente, argumento de deploy ou configuração externa.
Isso gera alguns efeitos ruins:
- a regra de negócio não aparece onde o caso de uso acontece;
- o comportamento ativo depende do modo como a aplicação foi iniciada;
- testar combinações fica mais trabalhoso;
- ler o sistema exige conhecer bootstrap, e não só o fluxo do código.
Esse é o mesmo tipo de distorção discutido no post sobre configuração importar mais do que parece: no Spring, configuração define contexto de execução, mas isso não significa que toda variação deveria morar nela.
Se a diferença é funcional, ela precisa aparecer como parte do desenho da aplicação.
Quando a variação é do domínio, a decisão precisa existir no domínio
Se varejo e atacado são modos legítimos do sistema, os dois comportamentos deveriam poder existir ao mesmo tempo.
Nesse caso, a seleção precisa ser explícita no fluxo:
public interface CalculadoraPreco {
TipoTabela suporta();
BigDecimal calcular(Pedido pedido);
}
@Service
public class CalculadoraPrecoVarejo implements CalculadoraPreco {
@Override
public TipoTabela suporta() {
return TipoTabela.VAREJO;
}
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getSubtotal().multiply(new BigDecimal("1.10"));
}
}
@Service
public class CalculadoraPrecoAtacado implements CalculadoraPreco {
@Override
public TipoTabela suporta() {
return TipoTabela.ATACADO;
}
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getSubtotal().multiply(new BigDecimal("0.92"));
}
}
@Component
public class CalculadoraPrecoFactory {
private final Map<TipoTabela, CalculadoraPreco> calculadoras;
public CalculadoraPrecoFactory(List<CalculadoraPreco> calculadoras) {
this.calculadoras = calculadoras.stream()
.collect(Collectors.toMap(CalculadoraPreco::suporta, Function.identity()));
}
public CalculadoraPreco para(TipoTabela tipoTabela) {
return Optional.ofNullable(calculadoras.get(tipoTabela))
.orElseThrow();
}
}
Agora a aplicação consegue carregar as duas estratégias e decidir em tempo de execução qual delas usar.
A diferença é grande:
- o comportamento deixa de depender do ambiente de subida;
- a regra fica visível no fluxo da aplicação;
- o sistema suporta cenários combinados sem multiplicar deploys;
- o código expressa uma variação de domínio, não uma mutação de bootstrap.
Em resumo: se os dois comportamentos fazem parte do mesmo produto, eles deveriam existir como código da mesma aplicação.
@Profile faz sentido quando a diferença é de ambiente
Isso não significa que @Profile seja ruim.
Significa só que ele tem um papel específico.
Ele é útil quando a diferença não é de regra de negócio, e sim de contexto operacional.
Por exemplo:
public interface GatewayPagamento {
void cobrar(Pedido pedido);
}
@Configuration
public class GatewayPagamentoConfig {
@Bean
@Profile("prod")
public GatewayPagamento gatewayPagamentoHttp() {
return new GatewayPagamentoHttp();
}
@Bean
@Profile({"dev", "test"})
public GatewayPagamento gatewayPagamentoFake() {
return new GatewayPagamentoFake();
}
}
Aqui a diferença é coerente com o mecanismo.
Em produção, você quer integração real.
Em desenvolvimento ou teste, talvez queira uma implementação fake, mais barata, previsível e sem dependência externa.
Nesse cenário, profile está descrevendo ambiente.
Não está decidindo regra do domínio.
Ele também costuma fazer sentido para:
- integrações reais versus stubs locais;
- observabilidade mais pesada em produção;
- infraestrutura opcional em desenvolvimento;
- adaptações de runtime que dependem do ambiente, não do caso de uso.
O erro começa quando profile vira substituto de modelagem
Existe um cheiro comum em projetos Spring:
@Profile("boleto")@Profile("cartao")@Profile("cliente-a")@Profile("cliente-b")@Profile("fluxo-antigo")@Profile("fluxo-novo")
Quando você vê isso, vale suspeitar.
Porque quase sempre o profile está sendo usado para esconder uma decisão que deveria estar em código, property de feature flag, estratégia explícita ou composição de módulos.
O ponto central é este:
@Profile escolhe o que existe no container.
Organização de código escolhe como a aplicação representa suas responsabilidades e variações.
Essas coisas não são iguais.
O ponto que vale fixar
Se a diferença é de ambiente, @Profile pode ser uma boa ferramenta.
Se a diferença é de negócio, produto, cliente, canal, fluxo ou tipo de operação, você provavelmente está tentando usar configuração para resolver um problema de modelagem.
E isso quase nunca simplifica de verdade.
Só faz a aplicação parecer mais limpa enquanto desloca a decisão mais importante para fora do lugar onde ela deveria ser lida.
