quarkusio/quarkus · error · IllegalArgumentException

@ExactlyOnce on method ${methodName} requires both @Incoming

Error message

@ExactlyOnce on method ${methodName} requires both @Incoming and @Outgoing annotations

What it means

The Kafka connector's @ExactlyOnce processing mode requires a method annotated @ExactlyOnce to be a full processing step: it must have both @Incoming and @Outgoing so the framework can coordinate commit of incoming offsets with publishing outgoing records. A method missing either annotation fails deployment with IllegalArgumentException.

Source

Thrown at extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java:150

            CombinedIndexBuildItem combinedIndex,
            List<ConnectorManagedChannelBuildItem> channelsManagedByConnectors,
            BuildProducer<RunTimeConfigurationDefaultBuildItem> defaultConfigProducer,
            BuildProducer<InjectedEmitterBuildItem> emitters) {

        DefaultSerdeDiscoveryState discoveryState = new DefaultSerdeDiscoveryState(combinedIndex.getIndex());

        for (AnnotationInstance annotation : combinedIndex.getIndex().getAnnotations(DotNames.EXACTLY_ONCE)) {
            if (annotation.target().kind() != AnnotationTarget.Kind.METHOD) {
                continue;
            }
            MethodInfo method = annotation.target().asMethod();
            String methodName = method.declaringClass().name() + "#" + method.name();

            AnnotationInstance incoming = method.annotation(DotNames.INCOMING);
            AnnotationInstance outgoing = method.annotation(DotNames.OUTGOING);

            if (incoming == null || outgoing == null) {
                throw new IllegalArgumentException(
                        "@ExactlyOnce on method " + methodName + " requires both @Incoming and @Outgoing annotations");
            }

            if (method.parametersCount() == 0) {
                throw new IllegalArgumentException(
                        "@ExactlyOnce on method " + methodName + " requires at least one parameter");
            }

            for (Type paramType : method.parameterTypes()) {
                if (paramType.name().equals(DotNames.MESSAGE)) {
                    throw new IllegalArgumentException(
                            "@ExactlyOnce on method " + methodName
                                    + " does not support Message parameters, use payload types instead");
                }
            }

            if (method.returnType().name().equals(DotNames.VOID)
                    || method.returnType().name().equals(VOID_BOXED)) {

View on GitHub (pinned to e1c734241f)

Solutions

  1. Add both @Incoming and @Outgoing to the @ExactlyOnce method.
  2. If only consuming, remove @ExactlyOnce and configure the connector's processing.garbage-collection / commit strategy instead.
  3. Use normal at-least-once semantics with `quarkus.messaging.kafka... processing.failsafe` if a pure consumer is intended.

Example fix

// before
@Incoming("in")
@ExactlyOnce
void consume(String m) { ... }
// after
@Incoming("in")
@Outgoing("out")
@ExactlyOnce
String process(String m) { return transform(m); }
Defensive patterns

Strategy: validation

Validate before calling

fun validateExactlyOnce(methodAnnotations: Set<String>): Boolean =
    "ExactlyOnce" !in methodAnnotations ||
        ("Incoming" in methodAnnotations && "Outgoing" in methodAnnotations)

Prevention

When it happens

Trigger: Annotating a method with @ExactlyOnce but only @Incoming, only @Outgoing, or neither.

Common situations: Copy-pasting @ExactlyOnce onto a consumer-only method; misunderstanding that exactly-once applies to the processing chain, not a plain consumer.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/891b5ed69c655770. Report an issue: GitHub.