quarkusio/quarkus · error · IllegalArgumentException

@ExactlyOnce on method ${methodName} cannot combine @Transac

Error message

@ExactlyOnce on method ${methodName} cannot combine @Transactional with @WithTransaction

What it means

@Transactional (CDI interceptor) and @WithTransaction (reactive messaging annotation) are two mutually exclusive transaction mechanisms; applying both on an @ExactlyOnce method is ambiguous — the extension cannot decide which transaction manages the method. The build-time processor rejects the combination.

Source

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

            if (reactive && method.returnType().kind() == Type.Kind.PARAMETERIZED_TYPE) {
                DotName typeArg = method.returnType().asParameterizedType().arguments().get(0).name();
                if (typeArg.equals(VOID_BOXED)) {
                    throw new IllegalArgumentException(
                            "@ExactlyOnce on method " + methodName
                                    + " must return a value to produce to the outgoing channel");
                }
            }

            if (method.hasAnnotation(DotNames.WITH_TRANSACTION) && !reactive) {
                throw new IllegalArgumentException(
                        "@ExactlyOnce on method " + methodName
                                + " cannot combine @WithTransaction with a synchronous return type"
                                + ", use @Transactional instead");
            }

            if (method.hasAnnotation(DotNames.TRANSACTIONAL) && method.hasAnnotation(DotNames.WITH_TRANSACTION)) {
                throw new IllegalArgumentException(
                        "@ExactlyOnce on method " + methodName
                                + " cannot combine @Transactional with @WithTransaction");
            }

            boolean isSuspend = method.parameterTypes().stream()
                    .anyMatch(t -> t.name().equals(DotNames.CONTINUATION));
            if (isSuspend) {
                throw new IllegalArgumentException(
                        "@ExactlyOnce on method " + methodName
                                + " does not support Kotlin suspend functions");
            }

            String incomingChannel = incoming.value().asString();
            String outgoingChannel = outgoing.value().asString();

            if (!discoveryState.isKafkaConnector(channelsManagedByConnectors, true, incomingChannel)) {
                throw new IllegalArgumentException(
                        "@ExactlyOnce on method " + methodName

View on GitHub (pinned to e1c734241f)

Solutions

  1. Keep only @Transactional for synchronous methods.
  2. Keep only @WithTransaction for reactive (Uni/Multi/CompletionStage) methods.
  3. Remove the redundant annotation depending on the intended execution model.

Example fix

// before
@ExactlyOnce @Transactional @WithTransaction
@Incoming("in") @Outgoing("out")
public Uni<String> process(String p) { ... }

// after
@ExactlyOnce @WithTransaction
@Incoming("in") @Outgoing("out")
public Uni<String> process(String p) { ... }
Defensive patterns

Strategy: validation

Validate before calling

// reject double transaction annotations
if (method.isAnnotationPresent(Transactional.class) && method.isAnnotationPresent(WithTransaction.class)) {
    throw new IllegalStateException("Choose either @Transactional or @WithTransaction");
}

Prevention

When it happens

Trigger: A single @ExactlyOnce method carries both @jakarta.transaction.Transactional and @WithTransaction annotations simultaneously.

Common situations: Adding @WithTransaction to a method that already had @Transactional (or vice versa) during migration between sync and reactive styles.

Related errors


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