Skip to content

[#210] Add tenant-aware event processing to the multi-tenancy demo - #4795

Merged
laura-devriendt-lemon merged 2 commits into
mainfrom
feature/axoniq/176/210_pooled_streaming_demo
Jul 31, 2026
Merged

[#210] Add tenant-aware event processing to the multi-tenancy demo#4795
laura-devriendt-lemon merged 2 commits into
mainfrom
feature/axoniq/176/210_pooled_streaming_demo

Conversation

@laura-devriendt-lemon

@laura-devriendt-lemon laura-devriendt-lemon commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Demonstrates the tenant-aware pooled streaming read side from axoniq-framework#283 (axoniq-framework#210) in the multi-tenancy example.

Stacked on #4794, so the diff here is the single commit on top of it.

What it shows

The statistics read model becomes a real projection: one ordinary pooled streaming processor consumes every tenant's events and writes each into the read model of the tenant it came from. CourseStatisticsProjection names no tenant, since the framework puts the tenant of a streamed event on the processing context and resolves its @TenantScoped parameters from it. Nothing identifies the tenant inside the stored event.

The demo also counts what served all three tenants, finding one processor rather than one per tenant, and shows a tenant added at runtime being picked up with no configuration change. EnrollStudentCommandHandler now only appends, which is what its javadoc already promised.

Two design points

  • In memory the command handler still fills the read model. A shared event store leaves a streamed event with no tenant to attribute it to, so no projection could tell tenants apart there. ReadModelUpdates selects which one writes, and EnrollmentRecording is the single write both use, mirroring the existing TenantProvisioning and TenantSnapshots seams.
  • The read models are idempotent. CourseStatisticsStore counts enrolled student identifiers instead of incrementing, and AuditLog keys its entries. Events arrive at least once, and re-opening the stream on a tenant change can repeat one.

Depends on axoniq-framework#283

Examples CI fails here until that merges and a new snapshot publishes. Verified locally against one: demo unit tests green, and MultiTenancyDemoIT green against a containerized Axon Server, reporting one processor and springfield=2, shelbyville=2, ogdenville=1.

@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/209_event_storage_snapshot_demo branch from 1bc2cab to 27d0936 Compare July 29, 2026 09:57
@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/210_pooled_streaming_demo branch from 45bb8fc to d41a607 Compare July 29, 2026 10:02
@laura-devriendt-lemon laura-devriendt-lemon self-assigned this Jul 29, 2026
@laura-devriendt-lemon laura-devriendt-lemon added Type: Feature Use to signal an issue is completely new to the project. Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. labels Jul 29, 2026
@laura-devriendt-lemon laura-devriendt-lemon added this to the Release 5.3.0 milestone Jul 29, 2026
@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/210_pooled_streaming_demo branch 2 times, most recently from 4d8aa99 to 0524eb5 Compare July 29, 2026 11:08
@laura-devriendt-lemon

Copy link
Copy Markdown
Contributor Author

@smcvb Two rough edges in the Spring extension that this demo ran into. Neither is multi-tenancy related, both live in extensions/spring and neither mentions tenants, and the demo works around both. So this is only a question of where you want them handled.

1. A @Bean Module holding an event processor is silently ignored. Declaring a PooledStreamingEventProcessorModule as a @Bean Module, the way the command handling and entity modules are declared right above it, registers no processor at all: no Coordinator, no warning, no error. The projection simply never runs, with nothing in the log pointing at why. SpringComponentRegistry#scanForModules does pick the bean up (SpringComponentRegistry.java:594), so it is dropped somewhere after that.

2. A pooled streaming processor requires a TokenStore bean named literally tokenStore. EventProcessorProperties.java:156 defaults the name to "tokenStore", and SpringCustomizations.java:171-174 then treats a non-empty name as mandatory and fails startup when no bean matches it. Naming the bean anything else breaks the application. The declarative path needs no equivalent: it resolves a token store by type and falls back to an in-memory one.

The two interact badly. Declaring the handler as its own @Bean makes Spring assign it to a second processor named after the handler's package, and that processor is the one that raises the tokenStore error, so the message names a processor you never declared.

Both are reproducible through this PR's Spring Boot demo, which is where I hit them. Fix them now, open issues for them, or leave them as they are?

@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/210_pooled_streaming_demo branch 3 times, most recently from 75b7c33 to b2038b7 Compare July 29, 2026 12:24
@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/209_event_storage_snapshot_demo branch from 1b39c86 to 3688bb4 Compare July 30, 2026 08:50
@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/210_pooled_streaming_demo branch from b2038b7 to f853002 Compare July 30, 2026 09:30
Base automatically changed from feature/axoniq/176/209_event_storage_snapshot_demo to main July 30, 2026 09:34
@laura-devriendt-lemon
laura-devriendt-lemon force-pushed the feature/axoniq/176/210_pooled_streaming_demo branch from ea8de01 to fdef226 Compare July 30, 2026 13:23
@laura-devriendt-lemon
laura-devriendt-lemon marked this pull request as ready for review July 30, 2026 14:53
@laura-devriendt-lemon
laura-devriendt-lemon requested a review from a team as a code owner July 30, 2026 14:53
@laura-devriendt-lemon
laura-devriendt-lemon requested review from hjohn, jangalinski and smcvb and removed request for a team July 30, 2026 14:53
@smcvb

smcvb commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fix them now, open issues for them, or leave them as they are?

I do wonder how high a priority these are, though. If you run a normal app, you'd get a TokenStore, which AF automatically names tokenStore. You hit this issue because there's no auto-configured TokenStore.
Furthermore, Module based @Bean for Event Processors is likely not how users will register Event Processor in combination with Spring; they'd use the EventProcessorDefinition, as that's what we have documented.
So, an issue for both is fine, but I'd give them a priority 4 at the moment.

@smcvb smcvb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as with the previous demo - it's a lot and goes beyond showing a user how to just enable multi-tenancy for declarative and Spring Boot. But given our time window, fine enough, and a worthy additional test suite. So, approving this PR.

@laura-devriendt-lemon
laura-devriendt-lemon merged commit 8c84a99 into main Jul 31, 2026
5 of 7 checks passed
@laura-devriendt-lemon
laura-devriendt-lemon deleted the feature/axoniq/176/210_pooled_streaming_demo branch July 31, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. Type: Feature Use to signal an issue is completely new to the project.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants