Skip to content

fix: RevisionSnapshotFilter#type(Class) never matched stored snapshots, silently disabling revision filtering#4644

Open
MateuszNaKodach wants to merge 1 commit into
axon-4.13.xfrom
fix/af4/revision-snapshot-filter-type-class
Open

fix: RevisionSnapshotFilter#type(Class) never matched stored snapshots, silently disabling revision filtering#4644
MateuszNaKodach wants to merge 1 commit into
axon-4.13.xfrom
fix/af4/revision-snapshot-filter-type-class

Conversation

@MateuszNaKodach

@MateuszNaKodach MateuszNaKodach commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Background

This was reported by a customer: they configured the filter with their aggregate class via type(Class), but the resolved fully-qualified class name never matched the type their snapshots were actually stored under — and there was no warning or any other signal of the mismatch. The filter appeared correctly configured while revision filtering was silently not happening.

The bug

RevisionSnapshotFilter.Builder#type(Class) resolved the type via Class#getName() — the fully-qualified class name. Snapshots, however, are stored under the aggregate's declared type: @AggregateRoot#type() when set, and the simple class name otherwise.

Since a fully-qualified name never equals the declared type, the filter's type check never matched any stored snapshot. Per the SnapshotFilter contract, a type mismatch makes the filter abstain (return true) so that filters for different aggregates can be AND-combined. The result: every type(Class)-configured filter was a silent no-op — it looked installed, but never evaluated the revision, allowing outdated or corrupt snapshots through.

The fix

type(Class) now resolves the declared type exactly the way the framework stores it. The resolution lives in a new public AnnotatedAggregateMetaModelFactory#declaredTypeOf(Class), to which the metamodel's internal findDeclaredType now delegates — a single source of truth for declared-type resolution. This also makes the manual builder path consistent with AggregateConfigurer, which already resolves the declared type via the metamodel.

Why this is safe for a patch release, despite changing behavior

The commit carries a BREAKING CHANGE marker for visibility, but the previous behavior could not work: there was no configuration in which type(Class) produced a filter that actually filtered. Anyone using it had a no-op filter without knowing it. After this fix:

  • Users with type(Class) get the revision filtering they always intended to have — outdated snapshots are now correctly rejected (and the aggregate is rebuilt from events, the standard recovery path).
  • The only conceivable regression would be a custom snapshotter that stores snapshots under the fully-qualified class name and configures the filter via type(Class). Such a setup would have had a working filter by accident; it can keep the old behavior explicitly with type(MyAggregate.class.getName()) via the unchanged type(String) overload.

Tests

  • type(Class) resolution: simple name and @AggregateRoot(type = ...) override
  • The FQCN misconfiguration case, pinning down the abstain semantics
  • declaredTypeOf defaults and override
  • Serializer-independence of revision filtering (XStream and Jackson), since the revision is stored as metadata by the RevisionResolver, not in the serialized body

Notes for reviewers

  • A Javadoc note documents the polymorphic-aggregate limitation: snapshots of a polymorphic hierarchy are stored under the concrete subtype's declared type, so type(ParentClass) only matches parent-typed snapshots — same as the existing AggregateConfigurer behavior, not a regression.

…type(Class)

A RevisionSnapshotFilter compares the revision only when its configured
type matches the aggregate type stored on the snapshot. Snapshots are
stored under the aggregate's declared type, which is @AggregateRoot#type
when set and the simple class name otherwise. A fully-qualified class name
never matches that declared type, so test() returns true on the type check
and leaves the revision unverified - allowing outdated or corrupt snapshots
through even with a revision filter in place.

RevisionSnapshotFilter.builder().type(Class) now resolves the declared type
the same way the framework stores it, rather than the fully-qualified name.
The resolution lives in a reusable
AnnotatedAggregateMetaModelFactory#declaredTypeOf(Class), to which the
metamodel's findDeclaredType delegates, keeping a single source of truth for
declared-type resolution.

Tests store snapshots under the realistic declared (simple) type. Added
coverage for type(Class) resolution (simple name and @AggregateRoot
override), declaredTypeOf, a fully-qualified type misconfiguration, and
serializer-independence of revision filtering (XStream and Jackson).

BREAKING CHANGE: RevisionSnapshotFilter.builder().type(Class) resolves the
aggregate's declared type (@AggregateRoot#type or the simple class name)
instead of the fully-qualified class name. A type(Class) configuration that
matched nothing and allowed all snapshots will now filter by revision.
@MateuszNaKodach MateuszNaKodach self-assigned this Jun 10, 2026
@MateuszNaKodach MateuszNaKodach added Type: Bug Use to signal issues that describe a bug within the system. Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. labels Jun 10, 2026
@MateuszNaKodach MateuszNaKodach added this to the Release 4.13.2 milestone Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. Type: Bug Use to signal issues that describe a bug within the system.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant