Rate limit AOP
View MarkdownStatus: Stable Package:
dev.vertique.ratelimit.aopArtifact:vertique-rate-limit-aopDepends on:vertique-rate-limit-core,vertique-aop,vertique-coreAPI reference: javadoc.io
vertique-rate-limit-aop adapts one @RateLimited method declaration to the
provider-neutral programmatic rate-limiting API. It owns the annotation vocabulary,
delegation to the shared runtime selector-path resolver, and the Dagger binding for the
rate-limit aspect. The generic Vertique AOP processor generates the application
proxy and reflection-free method metadata; this module does not own proxy
generation, quota math, or backend behavior.
@RateLimited is transport-neutral: it is usable on any Dagger-provided
component — services, REST resources, and future MCP tools alike — because the
generic AOP proxy intercepts Dagger Provider.get() resolution, not a
transport-specific hook.
When To Use It
Section titled “When To Use It”Install RateLimitAopModule alongside RateLimitCoreModule (and
vertique-rate-limit-redis for CLUSTERED policies) when methods should declare
per-operation admission through the annotation rather than manual RateLimiters
calls. Add vertique-codegen-rate-limit to the compiler’s annotation-processor path
so declarations are validated at compile time.
Core Concepts
Section titled “Core Concepts”@RateLimited(policy = "search-quota", key = {"tenantId", "userId"})public Future<SearchResult> search(String tenantId, String userId, Query query) { ... }@RateLimited names exactly one policy and is non-repeatable in v1: numeric limits,
algorithm, mode, revision, and failure behavior never appear in the annotation, only
in the named policy’s configuration. key() is an ordered array of selector paths —
never a template or format string — following the shared
selector-path grammar documented in the Selector-path grammar section of the
dev.vertique:vertique-aop module reference.
subject (default EFFECTIVE_PRINCIPAL) and anonymous (default
SHARED_BUCKET) control identity- or origin-scoped keying (see
vertique-rate-limit-core’s RateLimitSubjectResolver/RateLimitAdapterSupport);
the origin-aware values are IP, ACTOR_OR_IP, and CLIENT_OR_IP. They use the
trusted RequestOrigin.clientIp() captured in the security context and fail closed
when it is absent. CLIENT_OR_IP falls back to that IP only when an authenticated
identity has no client facet; strict CLIENT remains fail-closed. cost (default
1) is the per-invocation cost. An application that uses any subject() other than NONE
must co-install the existing AuthModule/SecurityModule (or JwtAuthModule) so a
typed, possibly anonymous, SecurityContext exists before the aspect runs —
otherwise every caller resolves as anonymous.
@Aspect(ordering = 300) places @RateLimited outside @Cacheable(200)/
@CacheEvict(100) and inside @Timed(1000) in the generated interceptor chain.
When composed with @Cacheable, this ordering makes quota admission the outer
boundary: every invocation consumes one quota unit before the cache lookup,
including a cache hit. A cache miss enters the nested cache and resilience
pipeline only after admission; retries remain inside that single admission and
do not consume additional quota units.
Handle resolution happens once, at generated-proxy-constructor time: an unknown
policy name fails application startup, before any request is served, exactly as
@RateLimited’s handle-creation contract in vertique-rate-limit-core describes.
At invocation, resolved selector components are framed into a RateLimitKey
through the identity-framing seam; an empty framing result (an anonymous caller
under AnonymousRateLimitPolicy.BYPASS) bypasses admission entirely and the method
proceeds unlimited, otherwise the resolved handle’s guarded execute(key, cost, invocation::proceed) runs.
Self-invocation bypass. Calling an @RateLimited method on this from inside
the same bean, or constructing the bean directly instead of resolving it through
Dagger, bypasses the generated proxy and therefore bypasses admission entirely —
identical to @Cacheable’s documented behavior. Only calls that arrive through the
Dagger-resolved proxy are intercepted.
Key Classes
Section titled “Key Classes”@RateLimited
Section titled “@RateLimited”policy() (required), key() (default {}), subject() (default
RateLimitSubject.EFFECTIVE_PRINCIPAL), anonymous() (default
AnonymousRateLimitPolicy.SHARED_BUCKET), cost() (default 1).
The annotation is @Target(ElementType.METHOD) only and carries
@Aspect(ordering = 300). Place it on methods of Dagger-provided classes, or on an interface
method (including an inherited default) that a concrete implementing class inherits or
overrides in the same compilation — vertique-codegen-aop weaves the trigger onto that
implementor’s $AopProxy. An interface alone is not a proxy bean; without a concrete
implementor in the compilation, no proxy is generated and no admission occurs.
RateLimitAopModule
Section titled “RateLimitAopModule”Public abstract Dagger @Module. Its single @Binds method binds AspectProvider<RateLimited>
to the package-private RateLimitedAspect, which the generated proxy resolves to build the
interceptor for each @RateLimited method. RateLimitedAspect itself is not application API.
Install the module in the application component alongside RateLimitCoreModule, which supplies
the RateLimiters runtime the aspect depends on.
Module Dagger Bindings
Section titled “Module Dagger Bindings”| Binding | Kind | Description |
|---|---|---|
AspectProvider<RateLimited> |
@Binds |
The generated-proxy interceptor for @RateLimited methods |
Verification
Section titled “Verification”./mvnw -ntp -pl vertique-rate-limit/vertique-rate-limit-aop -am verifyDependencies
Section titled “Dependencies”| Artifact | Purpose |
|---|---|
vertique-rate-limit-core |
Programmatic rate-limiting API the aspect delegates to |
vertique-aop |
Generic proxy generation and AspectProvider/@Aspect machinery |
vertique-core |
Framework foundations |