Filtering

When implementing a Projection, you would add handler methods (methods annotated with either @Handler or @HandlerFor) in order to express, what the projection is interested in.

Factus will look at these methods in order to discover fact specifications. These fact specifications form a query which is sent to the FactCast server to create a fact-stream suited for this projection. In detail, for each handler method, a Projector inspects the method’s annotations and parameter types including their annotations to build a FactSpec object. This object contains at least the ns, type properties. Optionally the version property is set.

If you look at a FactSpec however, sometimes it makes sense to use additional filtering possibilities like

  • aggregateId
  • meta key/value pair (one or more) or even
  • JavaScript acting as a predicate.

If for a projection these filters are known in advance, you can use additional annotations to declare them:

  • @FilterByAggId
  • @FilterByAggIdProperty (can only be used on aggregates)
  • @FilterByScript
  • @FilterByMeta (can be used repeatedly)
  • @FilterByMetaExists (can be used repeatedly)
  • @FilterByMetaDoesNotExist (can be used repeatedly)

Some Examples

FilterByMeta

Let’s say, you only want to receive events that have a meta pair “priority”:“urgent” in their headers. Here, you would use code like:


@Handler
@FilterByMeta(key = "priority", value = "urgent")
protected void apply(UserCreated created) {
    // ...
}

This will add the additional filter defined by the @FilterByMeta annotation to FactSpec. As a result, the filtering now takes place at the server side instead of wasteful client side filtering (like in the body of the apply method). Only those Facts will be returned, that have a meta key-value-pair with a key of priority and a value of urgent.

FilterByMetaExists


@Handler
@FilterByMetaExists("priority")
protected void apply(UserCreated created) {
    // ...
}

This will add the additional filter defined by the @FilterByMetaExists annotation to FactSpec. Only those Facts will be returned, that have a meta key-value-pair with a key of priority no matter what the value is. FilterByMetaDoesNotExist works the opposite way, of course.

FilterByAggIdProperty


@Handler
@FilterByAggIdProperty("recommendedByUserId")
protected void apply(UserRecommended event) {
    // ...
}

This will add the additional filter defined by the @FilterByAggIdProperty annotation to FactSpec. Only those Facts will be returned, that have a payload that contains a path ‘recommendedByUserId’ that has a UUID value which matches the current Aggregate’s ID. This is the reason, this kind of filter is only valid on Aggregate projections. Note that you could also define a dot-separated path like ‘references.recommendedByUserId’ if that matches your EventObject. The use of Array expressions is not allowed here.

This filter is particularly useful, if you want to process events that reference your Aggregate, but only if your Aggregate has a particular role.

In the above example, without the filter the apply method would receive all UserRecommended Events, no matter if the role of the current Aggregate was recommender or referral. You would probably need to check within your apply method programmatically to decide to process or skip the event.

Last modified October 7, 2025 : #3717: added docs (4ac6891ca)