Patrones avanzados con @ConditionalOnMissingBean en arquitecturas Spring Boot

1. Configuración de fuente de datos por defecto

En sistemas distribuidos con múltiples orígenes de datos, conviene disponer de una configuración respaldo que se active únicamente cuando no existan definiciones explícitas. Esto evita errores en entornos donde se olvide configurar el acceso a base de datos.

@Configuration
public class DefaultPersistenceConfig {

    private static final String DRIVER = "org.h2.Driver";
    private static final String URL_TEMPLATE = "jdbc:h2:mem:defaultdb;DB_CLOSE_DELAY=-1";

    @Bean
    @ConditionalOnMissingBean(DataSource.class)
    public DataSource fallbackDataSource() {
        var config = new HikariConfig();
        config.setDriverClassName(DRIVER);
        config.setJdbcUrl(URL_TEMPLATE);
        config.setUsername("sa");
        config.setPassword("");
        return new HikariDataSource(config);
    }
}

Cualquier módulo que declare su propio DataSource impedirá que se cree esta instancia de respaldo, manteniendo la compatibilidad sin requerir ajustes manuales.

2. Sistema de caché intercambiable

Los diferentes servicios de un ecosistema pueden requerir estrategias de caché distintas. Mediante una interfaz común y una implementación condicional, cada equipo adopta la tecnología que mejor se ajuste a sus necesidades.

public interface CacheGateway<K, V> {
    void store(K key, V value);
    Optional<V> retrieve(K key);
    void evict(K key);
}

@Component
@ConditionalOnMissingBean(CacheGateway.class)
public class InMemoryCacheGateway<K, V> implements CacheGateway<K, V> {

    private final Map<K, V> storage = new ConcurrentHashMap<>();

    @Override
    public void store(K key, V value) {
        storage.put(key, value);
    }

    @Override
    public Optional<V> retrieve(K key) {
        return Optional.ofNullable(storage.get(key));
    }

    @Override
    public void evict(K key) {
        storage.remove(key);
    }
}

Si otro módulo proporciona una implementación basada en Redis o Caffeine, Spring la detectará primero y omitirá la versión en memoria.

3. Sobrescritura de componentes según entorno

Combinando @Profile con @ConditionalOnMissingBean se logra una separación limpia entre configuraciones de desarrollo, pruebas y producción, eliminando conflictos entre definiciones duplicadas.

public interface NotificationService {
    void send(String recipient, String message);
}

@Configuration
public class NotificationConfig {

    @Bean
    @Profile("dev")
    @ConditionalOnMissingBean(NotificationService.class)
    public NotificationService consoleNotifier() {
        return (recipient, message) -> 
            System.out.println("[DEV] To: " + recipient + " | " + message);
    }

    @Bean
    @Profile("prod")
    @ConditionalOnMissingBean(NotificationService.class)
    public NotificationService emailNotifier(JavaMailSender mailSender) {
        return (recipient, message) -> {
            var msg = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(msg);
            helper.setTo(recipient);
            helper.setText(message);
            mailSender.send(msg);
        };
    }
}

4. Puntos de extensión en arquitectura modular

Los sistemas basados en plugins necesitan definir contratos estables que permitan a módulos externos inyectar comportamiento personalizado sin modificar el núcleo.

public interface AuditHook {
    void onEvent(String eventType, Map<String, Object> payload);
}

@Service
public class AuditDispatcher {

    private final AuditHook hook;

    public AuditDispatcher(@Autowired(required = false) AuditHook hook) {
        this.hook = hook != null ? hook : new NoOpAuditHook();
    }

    public void dispatch(String type, Map<String, Object> data) {
        hook.onEvent(type, data);
    }

    static class NoOpAuditHook implements AuditHook {
        @Override
        public void onEvent(String eventType, Map<String, Object> payload) {
            // sin operación
        }
    }
}

@Configuration
public class AuditAutoConfig {

    @Bean
    @ConditionalOnMissingBean(AuditHook.class)
    public AuditHook defaultAuditHook() {
        return new AuditDispatcher.NoOpAuditHook();
    }
}

5. Sustitución por mocks en pruebas

Durante la ejecución de tests, es habitual reemplazar dependencias externas por dobles controlados. La configuración condicional permite inyectar estos mocks sin interferir con las definiciones de producción.

@SpringBootTest
@ActiveProfiles("test")
class PaymentProcessingTest {

    @TestConfiguration
    static class MockConfig {

        @Bean
        @Primary
        @ConditionalOnMissingBean(PaymentGateway.class)
        public PaymentGateway mockPaymentGateway() {
            PaymentGateway mock = Mockito.mock(PaymentGateway.class);
            when(mock.charge(anyString(), any(BigDecimal.class)))
                .thenReturn(new PaymentResult("MOCK-TXN-001", true));
            return mock;
        }
    }

    @Autowired
    private PaymentService paymentService;

    @Test
    void shouldProcessPaymentWithMockedGateway() {
        var outcome = paymentService.processOrder("order-789", new BigDecimal("99.99"));
        
        assertTrue(outcome.isSuccessful());
        assertEquals("MOCK-TXN-001", outcome.getTransactionId());
    }
}

Los cinco patrones expuestos demuestran cómo @ConditionalOnMissingBean facilita la construcción de sistemas desacoplados: garantiza valores por defecto seguros, habilita estrategias de sustitución limpia, simplifica la gestión multi-entorno, soporta arquitecturas extensibles y agiliza la configuración de pruebas.

Etiquetas: Spring Boot ConditionalOnMissingBean Autoconfiguración Microservicios arquitectura modular

Publicado el 7-9 00:56