Class OutboxAutoConfiguration

java.lang.Object
com.aim2be.platform.outbox.OutboxAutoConfiguration

@AutoConfiguration @AutoConfigureAfter({org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class,org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration.class}) @Conditional(com.aim2be.platform.outbox.OnPgOutboxBackendCondition.class) @EnableConfigurationProperties(OutboxProperties.class) @Import(OutboxAutoConfigurationPackagesRegistrar.class) @EnableScheduling public class OutboxAutoConfiguration extends Object
Spring Boot auto-configuration for the outbox-publisher module.

Activated when im2be.outbox.enabled=true AND im2be.outbox.backend is PG (the default). The enabled default is false — every consuming service opts in explicitly per ADR-0014 §library-governance.

Backend gate (Wave A.2 — resolves the Phase 0a B2 deferred item). This is the Postgres auto-configuration: it wires JPA-backed beans (OutboxRecordRepository, OutboxPublishCompletion, a ShedLock JDBC LockProvider) that require a DataSource + spring-jdbc on the classpath. A Redis-only consumer (identity-service: im2be.outbox.backend=REDIS) has neither, so this whole graph must NOT activate for it — otherwise condition evaluation fails on the absent JdbcTemplateLockProvider. The OnPgOutboxBackendCondition gates the entire PG graph on enabled + backend == PG, compared case-INSENSITIVELY (parity with @ConditionalOnProperty's havingValue + Spring's relaxed enum binding, so enabled=TRUE and a lower-case backend: pg still match; reviewer R1) by reading the Environment directly in Java — NOT via a @ConditionalOnExpression with ${…} interpolation, which embeds the raw property value into the parsed SpEL string and is a SpEL-injection surface (rule 1; reviewer R2 — full rationale in that condition's Javadoc). The backend default is PG, so the 7 PG services with enabled=true and no explicit backend are unchanged; the Redis backend's own auto-configuration (RedisOutboxAutoConfiguration, Phase 0b) activates on backend=REDIS and supplies the OutboxBackend bean instead.

Wires:

  • OutboxRecord + OutboxRecordRepository via the OutboxAutoConfigurationPackagesRegistrar (additive entity + repository-package registration that does NOT clobber the consumer's @SpringBootApplication-derived scan). Spring Boot's JpaRepositoriesAutoConfiguration scans the packages registered through AutoConfigurationPackages — both the consumer's own packages and the outbox library's package — so repositories on both sides are picked up without the library holding @EnableJpaRepositories (which would register a JpaRepositoryFactoryBean bean and disable the consumer's autoconfig via its @ConditionalOnMissingBean guard, leaving consumer repositories unscanned — R7 finding).
  • OutboxBackend — storage SPI (Wave A.2 Phase 0a); the default PgOutboxBackend wraps the JPA repository + the REQUIRES_NEW completion helper. Selected by im2be.outbox.backend (default PG; only PG exists in Phase 0a). @ConditionalOnMissingBean so a consumer can supply an alternative without re-declaring the publisher/poller.
  • OutboxPublisher — hot AFTER_COMMIT relay (delegates storage to the OutboxBackend).
  • OutboxPollerWorker — cold scheduled poller (conditional on im2be.outbox.poller.enabled=true, default true; delegates storage to the OutboxBackend).
  • OutboxPublishCompletion — REQUIRES_NEW transactional helper (the PgOutboxBackend's transition collaborator).
  • OutboxMetricsBinder — Micrometer gauges + per-topic timers/counters (no-op when no MeterRegistry bean is present, so consumers without Actuator can still bootstrap).
  • Resilience4j CircuitBreakerRegistry with the im2be-outbox-relay circuit-breaker pre-registered.
  • ShedLock LockProvider backed by the application's DataSource (JDBC template provider).

Schedules (@EnableScheduling) and distributed locking (@EnableSchedulerLock) are activated only when the autoconfig is loaded — so consumers that wire ShedLock for other purposes still need to provide their own @EnableSchedulerLock when the outbox is disabled.

ShedLock default: @EnableSchedulerLock is required by ShedLock 6.x to carry defaultLockAtMostFor. The library binds the attribute to ${im2be.outbox.scheduler-lock.default-at-most:PT1M} via Spring's annotation-value property-placeholder resolution — consumers that don't want the library's PT1M default leaking into their own @SchedulerLock-annotated methods can override im2be.outbox.scheduler-lock.default-at-most in their own properties (or set per-method lockAtMostFor on each @SchedulerLock; ShedLock prefers the per-method value over the default). The library's own OutboxPollerWorker.pollAndPublishPending() sets lockAtMostFor + lockAtLeastFor explicitly via property placeholders, so it never relies on the default (R8 fix; previous hard-coded "PT1M" was unoverridable).

Registered via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.

  • Constructor Details

    • OutboxAutoConfiguration

      public OutboxAutoConfiguration()
  • Method Details

    • outboxCircuitBreakerRegistry

      @Bean @ConditionalOnMissingBean public io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry outboxCircuitBreakerRegistry()
      Default CircuitBreakerRegistry for consumers that haven't declared one. Vanilla ofDefaults() — the library does NOT impose its own thresholds on the registry default, because the registry-default config would silently leak into every OTHER breaker a consumer registers in the same registry. The outbox-specific thresholds live on outboxCircuitBreaker(OutboxProperties, CircuitBreakerRegistry) which uses an EXPLICIT CircuitBreakerConfig at breaker-creation time so the im2be.outbox.circuit-breaker.* properties are honoured regardless of which registry is in context.
    • outboxCircuitBreaker

      @Bean @ConditionalOnMissingBean(name="outboxCircuitBreaker") public io.github.resilience4j.circuitbreaker.CircuitBreaker outboxCircuitBreaker(OutboxProperties properties, io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry registry)
      The outbox-relay CircuitBreaker with thresholds explicitly built from OutboxProperties.getCircuitBreaker(). R4 fix — previous wiring let outboxPublisher pull the breaker from the CircuitBreakerRegistry via name lookup, which silently used the registry's default config when a consumer-provided registry shadowed the library's. By creating the breaker here with an explicit CircuitBreakerConfig, the im2be.outbox.circuit-breaker.* properties are guaranteed to take effect regardless of which registry the consumer wired.
      Parameters:
      properties - source of failure-rate-threshold + wait-duration-in-open-state-ms
      registry - the registry the breaker is registered into; uses CircuitBreakerRegistry.circuitBreaker(String, CircuitBreakerConfig) so the registered breaker carries the explicit config rather than the registry's default
    • outboxLockProvider

      @Bean @ConditionalOnMissingBean public net.javacrumbs.shedlock.core.LockProvider outboxLockProvider(DataSource dataSource)
      ShedLock provider backed by the application DataSource. Required by EnableSchedulerLock; the consuming service is expected to provide a shedlock table (the library does NOT ship the migration; consuming services author their own).
    • outboxOpenTelemetryFallback

      @Bean @ConditionalOnMissingBean public io.opentelemetry.api.OpenTelemetry outboxOpenTelemetryFallback()
      Falls back to the OpenTelemetry no-op SDK when no real OpenTelemetry bean is present — ensures the autoconfig boots in test contexts and dev environments that haven't wired OTel yet, without compromising the production observability story.
    • outboxMetricsBinder

      @Bean public OutboxMetricsBinder outboxMetricsBinder(org.springframework.beans.factory.ObjectProvider<io.micrometer.core.instrument.MeterRegistry> meterRegistry, OutboxRecordRepository repository)
      Metrics binder. Registers the two gauges at startup when a MeterRegistry bean is present; otherwise becomes a no-op so consumers without Spring Boot Actuator (or Micrometer on the classpath) can still bootstrap. The publisher and poller both depend on this bean unconditionally; the no-op fallback keeps that wiring intact.
    • outboxPublishCompletion

      @Bean public OutboxPublishCompletion outboxPublishCompletion(OutboxRecordRepository repository)
      REQUIRES_NEW completion helper. Distinct bean so the Spring transaction proxy intercepts each call (self-invocation would bypass the proxy). The PgOutboxBackend's transition collaborator.

      TODO(Phase 0b): this bean — together with OutboxRecordRepository and the PG backend — is JPA-specific. When the Redis backend module lands, the entire PG bean graph (repository registration, this helper, and the PG OutboxBackend) must become conditional on the selected backend (e.g. @ConditionalOnProperty im2be.outbox.backend=PG or JPA-classpath presence) so a Redis-only consumer never triggers JPA wiring. Deferred to Phase 0b per the design (backend-selection wiring is explicitly Phase 0b scope — reviewer R1 B2).

    • outboxBackend

      @Bean @ConditionalOnMissingBean public OutboxBackend outboxBackend(OutboxRecordRepository repository, OutboxPublishCompletion completion, OutboxProperties properties)
      Default OutboxBackend — the Postgres backend (Wave A.2 Phase 0a). Wraps the JPA OutboxRecordRepository (persist + FIFO batch fetch) and the OutboxPublishCompletion REQUIRES_NEW helper (the status-guarded transitions). Selected unconditionally for now — OutboxProperties.getBackend() defaults to OutboxProperties.Backend.PG and only PG exists in Phase 0a; the Redis backend (Phase 0b) plugs in via the same SPI in a separate module.

      @ConditionalOnMissingBean lets a consumer override the backend (e.g. a test fake or the future Redis impl) without re-declaring the publisher + poller wiring.

      A non-PG value for im2be.outbox.backend is honoured by Spring's relaxed enum binding but has no implementation in Phase 0a; rather than fail silently (reviewer R1 A1), a startup WARN records the fallback to PgOutboxBackend so the misconfiguration is visible in logs. The actual backend switch lands in Phase 0b.

    • outboxPublisher

      @Bean public OutboxPublisher outboxPublisher(OutboxBackend backend, org.springframework.kafka.core.KafkaTemplate<byte[],byte[]> kafkaTemplate, io.github.resilience4j.circuitbreaker.CircuitBreaker outboxCircuitBreaker, io.opentelemetry.api.OpenTelemetry openTelemetry, OutboxMetricsBinder metrics, OutboxProperties properties)
      AFTER_COMMIT hot relay bean. Wraps the Kafka send in the OutboxPublisher.CIRCUIT_BREAKER_NAME circuit-breaker. The breaker is injected by type so its config is the explicit one built in outboxCircuitBreaker(OutboxProperties, CircuitBreakerRegistry) (R4 fix — see that method's Javadoc). Storage is delegated to the OutboxBackend SPI (Wave A.2 Phase 0a) — the default PgOutboxBackend preserves the prior PG behaviour exactly.
    • outboxPollerWorker

      @Bean @ConditionalOnProperty(prefix="im2be.outbox.poller", name="enabled", havingValue="true", matchIfMissing=true) public OutboxPollerWorker outboxPollerWorker(OutboxBackend backend, org.springframework.kafka.core.KafkaTemplate<byte[],byte[]> kafkaTemplate, OutboxProperties properties, io.opentelemetry.api.OpenTelemetry openTelemetry, OutboxMetricsBinder metrics)
      Cold poller bean. Wires a @Scheduled sweep over PENDING rows. Conditional on im2be.outbox.poller.enabled=true (default true); consumers may disable to inspect PENDING rows by hand. Storage is delegated to the OutboxBackend SPI (Wave A.2 Phase 0a).