soda docs v17.0.1

Configuration

soda's configuration surface is small: one bus-type switch + a set of Redis options + one thread pool. This page covers how to configure them, where they go, and the three easiest pitfalls. For the complete key-by-key list see Configuration Reference.

Where configuration goes

soda uses Spring Boot's standard mechanism: application.yml (or .properties), with profiles for per-environment isolation.

yaml
# application.yml — shared by all environments
soda:
  event:
    bus-type: simple          # write it explicitly, don't rely on the default

scoda:
  ddd:
    cqrs:
      async:
        core-pool-size: 8
yaml
# application-prod.yml — production overrides
soda:
  event:
    bus-type: redis
    redis:
      host: ${SODA_REDIS_HOST}
      password: ${SODA_REDIS_PASSWORD}
Important

Credentials go in environment variables, never in the yml. Values like soda.event.redis.password use an ${ENV_VAR} placeholder; the real value lives in the deployment environment's variables. yml files get committed — committed files must contain zero plaintext secrets.

Minimal usable configuration

Three situations — pick the one that matches yours:

Single instance / development / testing: in-process bus

yaml
soda:
  event:
    bus-type: simple

Done. No Redis needed; events are dispatched synchronously inside one JVM.

Production / multiple instances: Redis event bus

yaml
soda:
  event:
    bus-type: redis
    redis:
      host: ${SODA_REDIS_HOST}
      port: 6379
      password: ${SODA_REDIS_PASSWORD}
      database: 0
      topic: soda-events

Prerequisites:

  • The soda-event-redis dependency is on the classpath (see Installation)
  • The project has a usable RedisTemplate / StringRedisTemplate (Spring Data Redis auto-configuration provides one)

Configure nothing

It runs, but don't.

With bus-type unset, SimpleEventBusAutoConfiguration's matchIfMissing = true activates the in-process bus. The behavior is correct, but the intent is unclear — six months later nobody knows whether Simple was the plan or the key was simply forgotten.

Warning

Do not write bus-type: spring. The soda-event-spring module was removed in 17.0.1, but EventProperties still defaults to "spring". The consequence of explicitly configuring spring: not a single EventBus bean gets created, RepositoryEventAutoConfiguration (which depends on @ConditionalOnBean(EventBus.class)) doesn't register either, and CqrsAroundHandler in SpringBusAutoConfiguration hard-depends on EventBus — the application throws NoSuchBeanDefinitionException at startup, not silent event loss at runtime.

When migrating old projects from v16 and earlier, this is the one most easily carried into production.

Redis bus options worth tuning

The defaults get you through development; for production, look at these:

KeyDefaultWhen to change it
soda.event.redis.topicsoda-eventsWhen several businesses share one Redis, isolate them with different topics
...stream.group-namesoda-events-groupWhen one topic has multiple independent consumers, give each its own consumer group
...stream.consumer-namesoda-events-consumer-<port>On multi-instance deployments it must be unique per instance (the default already distinguishes by port)
...stream.concurrency1Raise it when consumption throughput falls short — but then EventHandlers must be idempotent
...stream.batch-size10How many entries are pulled from the Stream per fetch
...stream.maxlen10000Stream length cap, to stop unbounded growth from eating memory
...stream.max-retries3Failure retry count; beyond that, entries go to the dead-letter stream
...stream.dead-letter-streamsoda-events-dead-letterThe dead-letter stream's key name
Caution

consumer-name must be unique on multi-instance deployments. Two instances sharing one consumer name make Redis treat them as the same consumer — one of them snatches every message while the other never receives anything, and their consumption offsets trample each other. The default includes server.port; if multiple instances share a port (common in containers), you must explicitly override it with the Pod name or hostname.

Idempotency

Redis Stream is at-least-once delivery — the same event may be consumed more than once. Recommended for cross-service scenarios:

yaml
soda:
  event:
    redis:
      stream:
        idempotency:
          enabled: true
          redis-key-prefix: soda-events-idempotency
          expire-time: 86400          # seconds, default 24 hours

With it on, the consumer first checks the eventId's processing state: SUCCESS / PROCESSING are skipped outright, FAILED is allowed to retry.

Note

Idempotency depends on the event's eventId being stable. soda's event IDs are snowflake-generated, and the same event object keeps its ID when redelivered — that is the precondition idempotency relies on. If you new an event object yourself and resend it, the ID changes and idempotency stops working.

Async thread pool

CQRS's async exits (sendAsyncCommand and friends) run on a dedicated thread pool:

yaml
scoda:
  ddd:
    cqrs:
      async:
        core-pool-size: 8
        max-pool-size: 16
        queue-capacity: 100
        thread-name-prefix: cqrs-async-
Warning

The prefix is scoda, not soda. This is a historical spelling slip — the soda.ddd.cqrs.async written in the README has no effect. Writing the wrong prefix raises no error; the thread pool silently keeps the defaults.

When the queue fills up, the rejection policy is run on the caller's thread (no tasks are dropped, but the calling thread blocks). So when queue-capacity is saturated, Web request threads get stuck along with it. For production:

  • Don't set queue-capacity too large — the queue is a buffer, not a warehouse
  • Monitor the pool's active count and queue depth
  • To take over completely, define your own Executor bean named cqrsAsyncExecutor

The three most common misconfigurations

SymptomRoot causeFix
Not a single domain event fires, no errorbus-type: spring was configured, or a nonexistent key like soda.ddd... was writtenWrite simple or redis explicitly; use @ConfigurationProperties validation to confirm key names
Thread pool options have no effectThe prefix was written as soda.ddd.cqrs.asyncChange it to scoda.ddd.cqrs.async
On multi-instance deployments only one machine receives eventsDuplicate consumer-nameInject a unique value per instance (Pod name / hostname)

Configuration validation

EventProperties carries @Validated, so misconfigurations fail at startup instead of surfacing at runtime:

KeyConstraint
soda.event.bus-type@NotBlank
soda.event.redis.port@Positive
soda.event.redis.database@PositiveOrZero
soda.event.redis.stream.concurrency etc.@Positive
soda.event.redis.stream.max-retries@PositiveOrZero

A Binding to target ... failed line in the startup log is this layer catching it — fix the key/value as the message suggests.

Next steps