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.
# 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# application-prod.yml — production overrides
soda:
event:
bus-type: redis
redis:
host: ${SODA_REDIS_HOST}
password: ${SODA_REDIS_PASSWORD}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
soda:
event:
bus-type: simpleDone. No Redis needed; events are dispatched synchronously inside one JVM.
Production / multiple instances: Redis event bus
soda:
event:
bus-type: redis
redis:
host: ${SODA_REDIS_HOST}
port: 6379
password: ${SODA_REDIS_PASSWORD}
database: 0
topic: soda-eventsPrerequisites:
- The
soda-event-redisdependency 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.
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:
| Key | Default | When to change it |
|---|---|---|
soda.event.redis.topic | soda-events | When several businesses share one Redis, isolate them with different topics |
...stream.group-name | soda-events-group | When one topic has multiple independent consumers, give each its own consumer group |
...stream.consumer-name | soda-events-consumer-<port> | On multi-instance deployments it must be unique per instance (the default already distinguishes by port) |
...stream.concurrency | 1 | Raise it when consumption throughput falls short — but then EventHandlers must be idempotent |
...stream.batch-size | 10 | How many entries are pulled from the Stream per fetch |
...stream.maxlen | 10000 | Stream length cap, to stop unbounded growth from eating memory |
...stream.max-retries | 3 | Failure retry count; beyond that, entries go to the dead-letter stream |
...stream.dead-letter-stream | soda-events-dead-letter | The dead-letter stream's key name |
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:
soda:
event:
redis:
stream:
idempotency:
enabled: true
redis-key-prefix: soda-events-idempotency
expire-time: 86400 # seconds, default 24 hoursWith it on, the consumer first checks the eventId's processing state: SUCCESS / PROCESSING are skipped outright, FAILED is allowed to retry.
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:
scoda:
ddd:
cqrs:
async:
core-pool-size: 8
max-pool-size: 16
queue-capacity: 100
thread-name-prefix: cqrs-async-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-capacitytoo 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
Executorbean namedcqrsAsyncExecutor
The three most common misconfigurations
| Symptom | Root cause | Fix |
|---|---|---|
| Not a single domain event fires, no error | bus-type: spring was configured, or a nonexistent key like soda.ddd... was written | Write simple or redis explicitly; use @ConfigurationProperties validation to confirm key names |
| Thread pool options have no effect | The prefix was written as soda.ddd.cqrs.async | Change it to scoda.ddd.cqrs.async |
| On multi-instance deployments only one machine receives events | Duplicate consumer-name | Inject a unique value per instance (Pod name / hostname) |
Configuration validation
EventProperties carries @Validated, so misconfigurations fail at startup instead of surfacing at runtime:
| Key | Constraint |
|---|---|
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
- Type, default, and validation rule of every key: Configuration Reference
- How the bus and aspect get wired up: Event Bus & Aspect
- How to replace the default implementations: Extension Points