diff --git a/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java b/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java
new file mode 100644
index 00000000..a8887ff9
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java
@@ -0,0 +1,265 @@
+package org.jboss.jandex;
+
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Annotation overlay allows overriding annotation information from an index. This is useful when
+ * Jandex is used as a language model and annotations are directly used as framework metadata.
+ * Transforming metadata is a frequent requirement in such situations, but core Jandex is immutable.
+ * This interface is layered on top of core Jandex and provides the necessary indirection between
+ * the user and the core Jandex that is required to apply the transformations.
+ *
+ * @since 3.2.0
+ */
+public interface AnnotationOverlay {
+    /**
+     * Returns a new builder for an annotation overlay for given {@code index} and a given collection
+     * of {@code transformations}.
+     *
+     * <p>
+     * <strong>Thread safety</strong>
+     *
+     * <p>
+     * The object returned by the builder is immutable and can be shared between threads without safe publication.
+     *
+     * @param index the Jandex index, must not be {@code null}
+     * @param annotationTransformations the collection of annotation transformations
+     * @return the annotation overlay builder, never {@code null}
+     */
+    static Builder builder(IndexView index, Collection<AnnotationTransformation> annotationTransformations) {
+        Objects.requireNonNull(index);
+        if (annotationTransformations == null) {
+            annotationTransformations = Collections.emptyList();
+        }
+        return new Builder(index, annotationTransformations);
+    }
+
+    /**
+     * Returns the index whose annotation information is being overlaid.
+     *
+     * @return the index underlying this annotation overlay, never {@code null}
+     */
+    IndexView index();
+
+    /**
+     * Returns whether an annotation instance with given {@code name} is declared on given {@code declaration}.
+     * <p>
+     * Like {@link AnnotationTarget#hasDeclaredAnnotation(DotName)}, and unlike {@link AnnotationTarget#hasAnnotation(DotName)},
+     * this method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param name name of the annotation type to look for, must not be {@code null}
+     * @return {@code true} if the annotation is present, {@code false} otherwise
+     */
+    boolean hasAnnotation(Declaration declaration, DotName name);
+
+    /**
+     * Returns whether an annotation instance of given {@code clazz} is declared on given {@code declaration}.
+     * <p>
+     * Like {@link AnnotationTarget#hasDeclaredAnnotation(Class)}, and unlike {@link AnnotationTarget#hasAnnotation(Class)},
+     * this method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param clazz the annotation type to look for, must not be {@code null}
+     * @return {@code true} if the annotation is present, {@code false} otherwise
+     * @see #hasAnnotation(Declaration, DotName)
+     */
+    default boolean hasAnnotation(Declaration declaration, Class<? extends Annotation> clazz) {
+        return hasAnnotation(declaration, DotName.createSimple(clazz.getName()));
+    }
+
+    /**
+     * Returns whether any annotation instance with one of given {@code names} is declared on given {@code declaration}.
+     * <p>
+     * This method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param names names of the annotation types to look for, must not be {@code null}
+     * @return {@code true} if any of the annotations is present, {@code false} otherwise
+     */
+    boolean hasAnyAnnotation(Declaration declaration, Set<DotName> names);
+
+    /**
+     * Returns whether any annotation instance of one of given {@code classes} is declared on given {@code declaration}.
+     * <p>
+     * This method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param classes annotation types to look for, must not be {@code null}
+     * @return {@code true} if any of the annotations is present, {@code false} otherwise
+     */
+    default boolean hasAnyAnnotation(Declaration declaration, Class<? extends Annotation>... classes) {
+        Set<DotName> names = new HashSet<>(classes.length);
+        for (Class<? extends Annotation> clazz : classes) {
+            names.add(DotName.createSimple(clazz.getName()));
+        }
+        return hasAnyAnnotation(declaration, names);
+    }
+
+    /**
+     * Returns the annotation instance with given {@code name} declared on given {@code declaration}.
+     * <p>
+     * Like {@link AnnotationTarget#annotation(DotName)}, and unlike {@link AnnotationTarget#annotation(DotName)},
+     * this method doesn't return annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param name name of the annotation type to look for, must not be {@code null}
+     * @return the annotation instance, or {@code null} if not found
+     */
+    AnnotationInstance annotation(Declaration declaration, DotName name);
+
+    /**
+     * Returns the annotation instance of given {@code clazz} declared on given {@code declaration}.
+     * <p>
+     * Like {@link AnnotationTarget#annotation(Class)}, and unlike {@link AnnotationTarget#annotation(Class)},
+     * this method doesn't return annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param clazz the annotation type to look for, must not be {@code null}
+     * @return the annotation instance, or {@code null} if not found
+     * @see #annotation(Declaration, DotName)
+     */
+    default AnnotationInstance annotation(Declaration declaration, Class<? extends Annotation> clazz) {
+        return annotation(declaration, DotName.createSimple(clazz.getName()));
+    }
+
+    /**
+     * Returns the annotation instances with given {@code name} declared on given {@code declaration}.
+     * If the specified annotation is repeatable, the result also contains all values from the container annotation
+     * instance.
+     * <p>
+     * The annotation class must be present in the index underlying this annotation overlay.
+     * <p>
+     * Like {@link AnnotationTarget#declaredAnnotationsWithRepeatable(DotName, IndexView)}, and unlike
+     * {@link AnnotationTarget#annotationsWithRepeatable(DotName, IndexView)}, this method doesn't return
+     * annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param name name of the annotation type, must not be {@code null}
+     * @return immutable collection of annotation instances, never {@code null}
+     */
+    Collection<AnnotationInstance> annotationsWithRepeatable(Declaration declaration, DotName name);
+
+    /**
+     * Returns the annotation instances of given type ({@code clazz}) declared on given {@code declaration}.
+     * If the specified annotation is repeatable, the result also contains all values from the container annotation
+     * instance.
+     * <p>
+     * The annotation class must be present in the index underlying this annotation overlay.
+     * <p>
+     * Like {@link AnnotationTarget#declaredAnnotationsWithRepeatable(Class, IndexView)}, and unlike
+     * {@link AnnotationTarget#annotationsWithRepeatable(Class, IndexView)}, this method doesn't return
+     * annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @param clazz the annotation type, must not be {@code null}
+     * @return immutable collection of annotation instances, never {@code null}
+     * @see #annotationsWithRepeatable(Declaration, DotName)
+     */
+    default Collection<AnnotationInstance> annotationsWithRepeatable(Declaration declaration,
+            Class<? extends Annotation> clazz) {
+        return annotationsWithRepeatable(declaration, DotName.createSimple(clazz.getName()));
+    }
+
+    /**
+     * Returns the annotation instances declared on given {@code declaration}.
+     * <p>
+     * Like {@link AnnotationTarget#declaredAnnotations()}, and unlike {@link AnnotationTarget#annotations()},
+     * this method doesn't return annotations declared on nested annotation targets. This doesn't hold in case of methods
+     * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered
+     * part of methods.
+     *
+     * @param declaration the declaration to inspect, must not be {@code null}
+     * @return immutable collection of annotation instances, never {@code null}
+     */
+    Collection<AnnotationInstance> annotations(Declaration declaration);
+
+    /**
+     * The builder for an annotation overlay.
+     */
+    final class Builder {
+        private final IndexView index;
+        private final Collection<AnnotationTransformation> annotationTransformations;
+
+        private boolean compatibleMode;
+        private boolean runtimeAnnotationsOnly;
+        private boolean inheritedAnnotations;
+
+        Builder(IndexView index, Collection<AnnotationTransformation> annotationTransformations) {
+            this.index = index;
+            this.annotationTransformations = annotationTransformations;
+        }
+
+        /**
+         * When called, the built annotation overlay shall treat method parameters as part of methods.
+         * This means that annotations on method parameters are returned when asking for annotations
+         * of a method, asking for annotations on method parameters results in an exception, and
+         * annotation transformations for method parameters are ignored.
+         * <p>
+         * This method is called {@code compatibleMode} because the built annotation overlay is
+         * compatible with the previous implementation of the same concept in Quarkus.
+         *
+         * @return this builder
+         */
+        public Builder compatibleMode() {
+            compatibleMode = true;
+            return this;
+        }
+
+        /**
+         * When called, the built annotation overlay shall only return runtime-retained annotations;
+         * class-retained annotations are ignored. Note that this only applies to annotations present
+         * in class files (and therefore in Jandex); annotations added to the overlay using
+         * {@linkplain AnnotationTransformation annotation transformations} are not inspected
+         * and are always returned.
+         *
+         * @return this builder
+         */
+        public Builder runtimeAnnotationsOnly() {
+            runtimeAnnotationsOnly = true;
+            return this;
+        }
+
+        /**
+         * When called, the built annotation overlay shall return {@linkplain java.lang.annotation.Inherited inherited}
+         * annotations per the Java rules.
+         *
+         * @return this builder
+         */
+        public Builder inheritedAnnotations() {
+            inheritedAnnotations = true;
+            return this;
+        }
+
+        /**
+         * Builds and returns an annotation overlay based on the configuration of this builder.
+         *
+         * @return the annotation overlay, never {@code null}
+         */
+        public AnnotationOverlay build() {
+            return new AnnotationOverlayImpl(index, compatibleMode, runtimeAnnotationsOnly, inheritedAnnotations,
+                    annotationTransformations);
+        }
+    }
+}
diff --git a/core/src/main/java/org/jboss/jandex/AnnotationOverlayImpl.java b/core/src/main/java/org/jboss/jandex/AnnotationOverlayImpl.java
new file mode 100644
index 00000000..e7bd1dfd
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/AnnotationOverlayImpl.java
@@ -0,0 +1,354 @@
+package org.jboss.jandex;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
+
+import org.jboss.jandex.AnnotationTransformation.TransformationContext;
+
+class AnnotationOverlayImpl implements AnnotationOverlay {
+    private static final Set<AnnotationInstance> SENTINEL = Collections.unmodifiableSet(new HashSet<>());
+
+    final IndexView index;
+    final boolean compatibleMode;
+    final boolean runtimeAnnotationsOnly;
+    final boolean inheritedAnnotations;
+    final List<AnnotationTransformation> transformations;
+    final Map<EquivalenceKey, Set<AnnotationInstance>> overlay = new ConcurrentHashMap<>();
+
+    AnnotationOverlayImpl(IndexView index, boolean compatibleMode, boolean runtimeAnnotationsOnly, boolean inheritedAnnotations,
+            Collection<AnnotationTransformation> annotationTransformations) {
+        this.index = index;
+        this.compatibleMode = compatibleMode;
+        this.runtimeAnnotationsOnly = runtimeAnnotationsOnly;
+        this.inheritedAnnotations = inheritedAnnotations;
+        if (!compatibleMode) {
+            for (AnnotationTransformation transformation : annotationTransformations) {
+                if (transformation.requiresCompatibleMode()) {
+                    throw new IllegalStateException("Compatible mode required by " + transformation);
+                }
+            }
+        }
+        List<AnnotationTransformation> transformations = new ArrayList<>(annotationTransformations);
+        transformations.sort(new Comparator<AnnotationTransformation>() {
+            @Override
+            public int compare(AnnotationTransformation o1, AnnotationTransformation o2) {
+                return Integer.compare(o2.priority(), o1.priority());
+            }
+        });
+        this.transformations = transformations;
+    }
+
+    @Override
+    public final IndexView index() {
+        return index;
+    }
+
+    @Override
+    public final boolean hasAnnotation(Declaration declaration, DotName name) {
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            throw new UnsupportedOperationException();
+        }
+
+        Collection<AnnotationInstance> annotations = getAnnotationsFor(declaration);
+        for (AnnotationInstance annotation : annotations) {
+            if (annotation.name().equals(name)) {
+                return true;
+            }
+        }
+
+        if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) {
+            ClassInfo clazz = index.getClassByName(declaration.asClass().superName());
+            while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) {
+                for (AnnotationInstance annotation : getAnnotationsFor(clazz)) {
+                    ClassInfo annotationClass = index.getClassByName(annotation.name());
+                    if (annotationClass != null
+                            && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)
+                            && annotation.name().equals(name)) {
+                        return true;
+                    }
+                }
+                clazz = index.getClassByName(clazz.superName());
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public final boolean hasAnyAnnotation(Declaration declaration, Set<DotName> names) {
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            throw new UnsupportedOperationException();
+        }
+
+        Collection<AnnotationInstance> annotations = getAnnotationsFor(declaration);
+        for (AnnotationInstance annotation : annotations) {
+            for (DotName name : names) {
+                if (annotation.name().equals(name)) {
+                    return true;
+                }
+            }
+        }
+
+        if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) {
+            ClassInfo clazz = index.getClassByName(declaration.asClass().superName());
+            while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) {
+                for (AnnotationInstance annotation : getAnnotationsFor(clazz)) {
+                    ClassInfo annotationClass = index.getClassByName(annotation.name());
+                    if (annotationClass != null && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)) {
+                        for (DotName name : names) {
+                            if (annotation.name().equals(name)) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+                clazz = index.getClassByName(clazz.superName());
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public final AnnotationInstance annotation(Declaration declaration, DotName name) {
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            throw new UnsupportedOperationException();
+        }
+
+        Collection<AnnotationInstance> annotations = getAnnotationsFor(declaration);
+        for (AnnotationInstance annotation : annotations) {
+            if (annotation.name().equals(name)) {
+                return annotation;
+            }
+        }
+
+        if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) {
+            ClassInfo clazz = index.getClassByName(declaration.asClass().superName());
+            while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) {
+                for (AnnotationInstance annotation : getAnnotationsFor(clazz)) {
+                    ClassInfo annotationClass = index.getClassByName(annotation.name());
+                    if (annotationClass != null
+                            && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)
+                            && annotation.name().equals(name)) {
+                        return annotation;
+                    }
+                }
+                clazz = index.getClassByName(clazz.superName());
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public final Collection<AnnotationInstance> annotationsWithRepeatable(Declaration declaration, DotName name) {
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            throw new UnsupportedOperationException();
+        }
+
+        DotName containerName = null;
+        {
+            ClassInfo annotationClass = index.getClassByName(name);
+            if (annotationClass != null) {
+                AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME);
+                if (repeatable != null) {
+                    containerName = repeatable.value().asClass().name();
+                }
+            }
+        }
+
+        List<AnnotationInstance> result = new ArrayList<>();
+        for (AnnotationInstance annotation : getAnnotationsFor(declaration)) {
+            if (annotation.name().equals(name)) {
+                result.add(annotation);
+            } else if (annotation.name().equals(containerName)) {
+                AnnotationInstance[] nestedAnnotations = annotation.value().asNestedArray();
+                for (AnnotationInstance nestedAnnotation : nestedAnnotations) {
+                    result.add(AnnotationInstance.create(nestedAnnotation, annotation.target()));
+                }
+            }
+        }
+
+        if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) {
+            ClassInfo clazz = index.getClassByName(declaration.asClass().superName());
+            while (result.isEmpty() && clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) {
+                for (AnnotationInstance annotation : getAnnotationsFor(clazz)) {
+                    ClassInfo annotationClass = index.getClassByName(annotation.name());
+                    if (annotationClass != null && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)) {
+                        if (annotation.name().equals(name)) {
+                            result.add(annotation);
+                        } else if (annotation.name().equals(containerName)) {
+                            AnnotationInstance[] nestedAnnotations = annotation.value().asNestedArray();
+                            for (AnnotationInstance nestedAnnotation : nestedAnnotations) {
+                                result.add(AnnotationInstance.create(nestedAnnotation, annotation.target()));
+                            }
+                        }
+                    }
+                }
+                clazz = index.getClassByName(clazz.superName());
+            }
+        }
+
+        return Collections.unmodifiableList(result);
+    }
+
+    @Override
+    public final Collection<AnnotationInstance> annotations(Declaration declaration) {
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            throw new UnsupportedOperationException();
+        }
+
+        Collection<AnnotationInstance> result = getAnnotationsFor(declaration);
+
+        if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) {
+            result = new ArrayList<>(result);
+            ClassInfo clazz = index.getClassByName(declaration.asClass().superName());
+            while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) {
+                for (AnnotationInstance annotation : getAnnotationsFor(clazz)) {
+                    ClassInfo annotationClass = index.getClassByName(annotation.name());
+                    if (annotationClass != null
+                            && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)
+                            && result.stream().noneMatch(it -> it.name().equals(annotation.name()))) {
+                        result.add(annotation);
+                    }
+                }
+                clazz = index.getClassByName(clazz.superName());
+            }
+        }
+
+        return Collections.unmodifiableCollection(result);
+    }
+
+    Set<AnnotationInstance> getAnnotationsFor(Declaration declaration) {
+        EquivalenceKey key = EquivalenceKey.of(declaration);
+        Set<AnnotationInstance> annotations = overlay.get(key);
+
+        if (annotations == null) {
+            Collection<AnnotationInstance> original = new HashSet<>(getOriginalAnnotations(declaration));
+            TransformationContextImpl transformationContext = new TransformationContextImpl(declaration, original);
+            for (AnnotationTransformation transformation : transformations) {
+                if (transformation.supports(declaration.kind())) {
+                    transformation.apply(transformationContext);
+                }
+            }
+            Set<AnnotationInstance> result = transformationContext.annotations;
+            annotations = original.equals(result) ? SENTINEL : Collections.unmodifiableSet(result);
+            overlay.put(key, annotations);
+        }
+
+        if (annotations == SENTINEL) {
+            annotations = getOriginalAnnotations(declaration);
+        }
+        return annotations;
+    }
+
+    final Set<AnnotationInstance> getOriginalAnnotations(Declaration declaration) {
+        Set<AnnotationInstance> result = new HashSet<>();
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD) {
+            for (AnnotationInstance annotation : declaration.asMethod().annotations()) {
+                if (annotation.target() != null
+                        && (annotation.target().kind() == AnnotationTarget.Kind.METHOD
+                                || annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER)
+                        && (!runtimeAnnotationsOnly || annotation.runtimeVisible())) {
+                    result.add(annotation);
+                }
+            }
+        } else {
+            for (AnnotationInstance annotation : declaration.declaredAnnotations()) {
+                if (!runtimeAnnotationsOnly || annotation.runtimeVisible()) {
+                    result.add(annotation);
+                }
+            }
+        }
+        return result;
+    }
+
+    private static final class TransformationContextImpl implements TransformationContext {
+        private final Declaration declaration;
+        private final Set<AnnotationInstance> annotations;
+
+        TransformationContextImpl(Declaration declaration, Collection<AnnotationInstance> annotations) {
+            this.declaration = declaration;
+            this.annotations = new HashSet<>(annotations);
+        }
+
+        @Override
+        public Declaration declaration() {
+            return declaration;
+        }
+
+        @Override
+        public Collection<AnnotationInstance> annotations() {
+            return annotations;
+        }
+
+        @Override
+        public boolean hasAnnotation(Class<? extends Annotation> annotationClass) {
+            Objects.requireNonNull(annotationClass);
+            return hasAnnotation(DotName.createSimple(annotationClass));
+        }
+
+        @Override
+        public boolean hasAnnotation(DotName annotationName) {
+            Objects.requireNonNull(annotationName);
+            for (AnnotationInstance annotation : annotations) {
+                if (annotation.name().equals(annotationName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean hasAnnotation(Predicate<AnnotationInstance> predicate) {
+            Objects.requireNonNull(predicate);
+            for (AnnotationInstance annotation : annotations) {
+                if (predicate.test(annotation)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void add(Class<? extends Annotation> annotationClass) {
+            Objects.requireNonNull(annotationClass);
+            annotations.add(AnnotationInstance.builder(annotationClass).build());
+        }
+
+        @Override
+        public void add(AnnotationInstance annotation) {
+            annotations.add(Objects.requireNonNull(annotation));
+        }
+
+        @Override
+        public void addAll(AnnotationInstance... annotations) {
+            Collections.addAll(this.annotations, Objects.requireNonNull(annotations));
+        }
+
+        @Override
+        public void addAll(Collection<AnnotationInstance> annotations) {
+            this.annotations.addAll(Objects.requireNonNull(annotations));
+        }
+
+        @Override
+        public void remove(Predicate<AnnotationInstance> predicate) {
+            annotations.removeIf(Objects.requireNonNull(predicate));
+        }
+
+        @Override
+        public void removeAll() {
+            annotations.clear();
+        }
+    }
+}
diff --git a/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java b/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java
new file mode 100644
index 00000000..c050e14c
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java
@@ -0,0 +1,884 @@
+package org.jboss.jandex;
+
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * An annotation transformation.
+ *
+ * @see #priority()
+ * @see #supports(AnnotationTarget.Kind)
+ * @see #apply(TransformationContext)
+ * @see #builder()
+ * @see #forClasses()
+ * @see #forFields()
+ * @see #forMethods()
+ * @see #forMethodParameters()
+ * @see #forRecordComponents()
+ * @since 3.2.0
+ */
+public interface AnnotationTransformation {
+    /**
+     * The default {@link #priority()} value: 1000.
+     */
+    int DEFAULT_PRIORITY_VALUE = 1000;
+
+    /**
+     * Returns the priority of this annotation transformation. Annotation transformations
+     * are applied in descending order of priority values (that is, transformation with
+     * higher priority value is executed sooner than transformation with smaller priority
+     * value).
+     * <p>
+     * By default, the priority is {@link #DEFAULT_PRIORITY_VALUE}.
+     *
+     * @return the priority of this annotation transformation
+     */
+    default int priority() {
+        return DEFAULT_PRIORITY_VALUE;
+    }
+
+    /**
+     * Returns whether this annotation transformation supports given {@link AnnotationTarget.Kind kind}
+     * of declarations. A transformation is only {@linkplain #apply(TransformationContext) applied}
+     * if it supports the correct kind of declarations.
+     * <p>
+     * By default, the transformation supports all declaration kinds.
+     *
+     * @param kind the kind of declaration, never {@code null}
+     * @return whether this annotation transformation should apply
+     */
+    default boolean supports(AnnotationTarget.Kind kind) {
+        return true;
+    }
+
+    /**
+     * Implements the actual annotation transformation.
+     *
+     * @param context the {@linkplain TransformationContext transformation context}, never {@code null}
+     */
+    void apply(TransformationContext context);
+
+    /**
+     * Returns whether this annotation transformation requires the annotation overlay to be
+     * in the {@linkplain AnnotationOverlay.Builder#compatibleMode() compatible mode}.
+     * When this method returns {@code true} and the annotation overlay is not set to be
+     * in the compatible mode, an exception is thrown during construction of the overlay.
+     * <p>
+     * This method returns {@code false} by default and should be overridden sparingly.
+     *
+     * @return whether this transformation requires the annotation overlay to be in the compatible mode
+     */
+    default boolean requiresCompatibleMode() {
+        return false;
+    }
+
+    /**
+     * A transformation context. Passed as a singular parameter to {@link #apply(TransformationContext)}.
+     *
+     * @see #declaration()
+     * @see #annotations()
+     * @see #hasAnnotation(Class)
+     * @see #hasAnnotation(DotName)
+     * @see #hasAnnotation(Predicate)
+     * @see #add(Class)
+     * @see #add(AnnotationInstance)
+     * @see #addAll(AnnotationInstance...)
+     * @see #addAll(Collection)
+     * @see #remove(Predicate)
+     * @see #removeAll()
+     */
+    interface TransformationContext {
+        /**
+         * Returns the declaration that is being transformed.
+         *
+         * @return the declaration that is being transformed
+         */
+        Declaration declaration();
+
+        /**
+         * Returns the collection of annotations present on the declaration that is being transformed.
+         * Reflects all changes done by this annotation transformation and all annotation transformations
+         * executed prior to this one.
+         * <p>
+         * Changes made directly to this collection and changes made through the other
+         * {@code TransformationContext} methods are interchangeable.
+         *
+         * @return the collection of annotations present on the declaration that is being transformed
+         */
+        Collection<AnnotationInstance> annotations();
+
+        /**
+         * Returns whether the {@linkplain #annotations() current set of annotations} contains
+         * an annotation of given {@code annotationClass}.
+         *
+         * @param annotationClass the annotation class, must not be {@code null}
+         * @return whether the current set of annotations contains an annotation of given class
+         */
+        boolean hasAnnotation(Class<? extends Annotation> annotationClass);
+
+        /**
+         * Returns whether the {@linkplain #annotations() current set of annotations} contains
+         * an annotation whose class has given {@code annotationName}.
+         *
+         * @param annotationName name of the annotation class, must not be {@code null}
+         * @return whether the current set of annotations contains an annotation of given class
+         */
+        boolean hasAnnotation(DotName annotationName);
+
+        /**
+         * Returns whether the {@linkplain #annotations() current set of annotations} contains
+         * an annotation that matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return whether the current set of annotations contains an annotation of given class
+         */
+        boolean hasAnnotation(Predicate<AnnotationInstance> predicate);
+
+        /**
+         * Adds an annotation of given {@code annotationClass} to
+         * the {@linkplain #annotations() current set of annotations}.
+         * <p>
+         * The annotation type must have no members.
+         *
+         * @param annotationClass the class of annotation to add, must not be {@code null}
+         */
+        void add(Class<? extends Annotation> annotationClass);
+
+        /**
+         * Adds the {@code annotation} to the {@linkplain #annotations() current set of annotations}.
+         *
+         * @param annotation the annotation to add, must not be {@code null}
+         */
+        void add(AnnotationInstance annotation);
+
+        /**
+         * Adds all {@code annotations} to the {@linkplain #annotations() current set of annotations}.
+         *
+         * @param annotations the annotations to add, must not be {@code null}
+         */
+        void addAll(AnnotationInstance... annotations);
+
+        /**
+         * Adds all {@code annotations} to the {@linkplain #annotations() current set of annotations}.
+         *
+         * @param annotations the annotations to add, must not be {@code null}
+         */
+        void addAll(Collection<AnnotationInstance> annotations);
+
+        /**
+         * Removes annotations that match given {@code predicate} from
+         * the {@linkplain #annotations() current set of annotations}.
+         *
+         * @param predicate the annotation predicate, must not be {@code null}
+         */
+        void remove(Predicate<AnnotationInstance> predicate);
+
+        /**
+         * Removes all annotations from {@linkplain #annotations() current set of annotations}.
+         */
+        void removeAll();
+    }
+
+    // ---
+
+    /**
+     * Returns a builder for annotation transformation of arbitrary declarations.
+     *
+     * @return a builder for annotation transformation of arbitrary declarations
+     */
+    static DeclarationBuilder builder() {
+        return new DeclarationBuilder();
+    }
+
+    /**
+     * Returns a builder for annotation transformation of classes.
+     *
+     * @return a builder for annotation transformation of classes
+     */
+    static ClassBuilder forClasses() {
+        return new ClassBuilder();
+    }
+
+    /**
+     * Returns a builder for annotation transformation of fields.
+     *
+     * @return a builder for annotation transformation of fields
+     */
+    static FieldBuilder forFields() {
+        return new FieldBuilder();
+    }
+
+    /**
+     * Returns a builder for annotation transformation of methods.
+     *
+     * @return a builder for annotation transformation of methods
+     */
+    static MethodBuilder forMethods() {
+        return new MethodBuilder();
+    }
+
+    /**
+     * Returns a builder for annotation transformation of method parameters.
+     *
+     * @return a builder for annotation transformation of method parameters
+     */
+    static MethodParameterBuilder forMethodParameters() {
+        return new MethodParameterBuilder();
+    }
+
+    /**
+     * Returns a builder for annotation transformation of record components.
+     *
+     * @return a builder for annotation transformation of record components
+     */
+    static RecordComponentBuilder forRecordComponents() {
+        return new RecordComponentBuilder();
+    }
+
+    /**
+     * Abstract class for {@linkplain AnnotationTransformation annotation transformation} builders.
+     *
+     * @see #priority(int)
+     * @see #whenAnyMatch(Class...)
+     * @see #whenAnyMatch(DotName...)
+     * @see #whenAnyMatch(List)
+     * @see #whenAnyMatch(Predicate)
+     * @see #whenAllMatch(Class...)
+     * @see #whenAllMatch(DotName...)
+     * @see #whenAllMatch(List)
+     * @see #whenAllMatch(Predicate)
+     * @see #whenNoneMatch(Class...)
+     * @see #whenNoneMatch(DotName...)
+     * @see #whenNoneMatch(List)
+     * @see #whenNoneMatch(Predicate)
+     * @see #when(Predicate)
+     * @see DeclarationBuilder
+     * @see ClassBuilder
+     * @see FieldBuilder
+     * @see MethodBuilder
+     * @see MethodParameterBuilder
+     * @see RecordComponentBuilder
+     * @param <THIS> type of this builder
+     */
+    abstract class Builder<THIS extends Builder<THIS>> {
+        private final AnnotationTarget.Kind kind;
+
+        private int priority;
+        private Predicate<TransformationContext> predicate;
+
+        Builder(AnnotationTarget.Kind kind) {
+            this.kind = kind;
+            this.priority = DEFAULT_PRIORITY_VALUE;
+        }
+
+        /**
+         * Sets the priority of the built annotation transformation.
+         * By default, the priority is {@link #DEFAULT_PRIORITY_VALUE}.
+         *
+         * @param priority the priority
+         * @return this builder
+         */
+        public final THIS priority(int priority) {
+            this.priority = priority;
+            return self();
+        }
+
+        @SafeVarargs
+        private static Predicate<AnnotationInstance> annotationPredicate(Class<? extends Annotation>... classes) {
+            Objects.requireNonNull(classes);
+            return annotation -> {
+                String annotationName = annotation.name().toString();
+                for (Class<? extends Annotation> clazz : classes) {
+                    if (annotationName.equals(clazz.getName())) {
+                        return true;
+                    }
+                }
+                return false;
+            };
+        }
+
+        private static Predicate<AnnotationInstance> annotationPredicate(DotName... classes) {
+            Objects.requireNonNull(classes);
+            return annotation -> {
+                DotName annotationName = annotation.name();
+                for (DotName clazz : classes) {
+                    if (annotationName.equals(clazz)) {
+                        return true;
+                    }
+                }
+                return false;
+            };
+        }
+
+        /**
+         * Adds a predicate that tests whether any of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * is of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        @SafeVarargs
+        public final THIS whenAnyMatch(Class<? extends Annotation>... classes) {
+            Objects.requireNonNull(classes);
+            return whenAnyMatch(annotationPredicate(classes));
+        }
+
+        /**
+         * Adds a predicate that tests whether any of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * is of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenAnyMatch(DotName... classes) {
+            Objects.requireNonNull(classes);
+            return whenAnyMatch(annotationPredicate(classes));
+        }
+
+        /**
+         * Adds a predicate that tests whether any of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * is of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenAnyMatch(List<DotName> classes) {
+            Objects.requireNonNull(classes);
+            return whenAnyMatch(classes.toArray(new DotName[0]));
+        }
+
+        /**
+         * Adds a predicate that tests whether any of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * matches the given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenAnyMatch(Predicate<AnnotationInstance> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> {
+                Collection<AnnotationInstance> annotations = ctx.annotations();
+                for (AnnotationInstance annotation : annotations) {
+                    if (predicate.test(annotation)) {
+                        return true;
+                    }
+                }
+                return false;
+            });
+        }
+
+        /**
+         * Adds a predicate that tests whether all of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * are of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        @SafeVarargs
+        public final THIS whenAllMatch(Class<? extends Annotation>... classes) {
+            Objects.requireNonNull(classes);
+            return whenAllMatch(annotationPredicate(classes));
+        }
+
+        /**
+         * Adds a predicate that tests whether all of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * are of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenAllMatch(DotName... classes) {
+            Objects.requireNonNull(classes);
+            return whenAllMatch(annotationPredicate(classes));
+        }
+
+        /**
+         * Adds a predicate that tests whether all of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * are of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenAllMatch(List<DotName> classes) {
+            Objects.requireNonNull(classes);
+            return whenAllMatch(classes.toArray(new DotName[0]));
+        }
+
+        /**
+         * Adds a predicate that tests whether all of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * match the given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenAllMatch(Predicate<AnnotationInstance> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> {
+                Collection<AnnotationInstance> annotations = ctx.annotations();
+                for (AnnotationInstance annotation : annotations) {
+                    if (!predicate.test(annotation)) {
+                        return false;
+                    }
+                }
+                return true;
+            });
+        }
+
+        /**
+         * Adds a predicate that tests whether none of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * is of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        @SafeVarargs
+        public final THIS whenNoneMatch(Class<? extends Annotation>... classes) {
+            Objects.requireNonNull(classes);
+            return whenNoneMatch(annotationPredicate(classes));
+        }
+
+        /**
+         * Adds a predicate that tests whether none of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * is of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenNoneMatch(DotName... classes) {
+            Objects.requireNonNull(classes);
+            return whenNoneMatch(annotationPredicate(classes));
+        }
+
+        /**
+         * Adds a predicate that tests whether none of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * is of given {@code classes}.
+         *
+         * @param classes the annotation classes, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenNoneMatch(List<DotName> classes) {
+            Objects.requireNonNull(classes);
+            return whenNoneMatch(classes.toArray(new DotName[0]));
+        }
+
+        /**
+         * Adds a predicate that tests whether none of
+         * the {@linkplain TransformationContext#annotations() current set of annotations}
+         * matches the given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public final THIS whenNoneMatch(Predicate<AnnotationInstance> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> {
+                Collection<AnnotationInstance> annotations = ctx.annotations();
+                for (AnnotationInstance annotation : annotations) {
+                    if (predicate.test(annotation)) {
+                        return false;
+                    }
+                }
+                return true;
+            });
+        }
+
+        /**
+         * Adds a predicate to the list of predicates that will be tested before applying the transformation.
+         * If some of the predicates returns {@code false}, the transformation is not applied. In other words,
+         * the predicates are combined using logical <em>and</em> (conjunction).
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         */
+        public THIS when(Predicate<TransformationContext> predicate) {
+            Objects.requireNonNull(predicate);
+            if (this.predicate == null) {
+                this.predicate = predicate;
+            } else {
+                this.predicate = this.predicate.and(predicate);
+            }
+            return self();
+        }
+
+        /**
+         * Builds an annotation transformation based on the given {@code transformation} function.
+         *
+         * @param transformation the transformation function, must not be {@code null}
+         * @return the built annotation transformation, never {@code null}
+         */
+        public AnnotationTransformation transform(Consumer<TransformationContext> transformation) {
+            Objects.requireNonNull(transformation);
+
+            return new AnnotationTransformation() {
+                @Override
+                public int priority() {
+                    return priority;
+                }
+
+                @Override
+                public boolean supports(AnnotationTarget.Kind kind) {
+                    return Builder.this.kind == null || Builder.this.kind == kind;
+                }
+
+                @Override
+                public void apply(TransformationContext context) {
+                    if (predicate == null || predicate.test(context)) {
+                        transformation.accept(context);
+                    }
+                }
+            };
+        }
+
+        @SuppressWarnings("unchecked")
+        THIS self() {
+            return (THIS) this;
+        }
+    }
+
+    /**
+     * A builder of {@linkplain AnnotationTransformation annotation transformations} for arbitrary declarations.
+     *
+     * @see #whenDeclaration(Predicate)
+     * @see Builder
+     */
+    class DeclarationBuilder extends Builder<DeclarationBuilder> {
+        DeclarationBuilder() {
+            super(null);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current declaration}
+         * matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public DeclarationBuilder whenDeclaration(Predicate<Declaration> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> predicate.test(ctx.declaration()));
+        }
+    }
+
+    /**
+     * A builder of {@linkplain AnnotationTransformation annotation transformations} for classes.
+     *
+     * @see #whenClass(Class)
+     * @see #whenClass(DotName)
+     * @see #whenClass(Predicate)
+     * @see Builder
+     */
+    class ClassBuilder extends Builder<ClassBuilder> {
+        ClassBuilder() {
+            super(AnnotationTarget.Kind.CLASS);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current class}
+         * is the given {@code clazz}.
+         *
+         * @param clazz the class, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public ClassBuilder whenClass(Class<?> clazz) {
+            Objects.requireNonNull(clazz);
+            return whenClass(DotName.createSimple(clazz));
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current class}
+         * has given {@code name}.
+         *
+         * @param name the class name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public ClassBuilder whenClass(DotName name) {
+            Objects.requireNonNull(name);
+            return whenClass(clazz -> clazz.name().equals(name));
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current class}
+         * matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public ClassBuilder whenClass(Predicate<ClassInfo> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> predicate.test(ctx.declaration().asClass()));
+        }
+    }
+
+    /**
+     * A builder of {@linkplain AnnotationTransformation annotation transformations} for fields.
+     *
+     * @see #whenField(Class, String)
+     * @see #whenField(DotName, String)
+     * @see #whenField(Predicate)
+     * @see Builder
+     */
+    class FieldBuilder extends Builder<FieldBuilder> {
+        FieldBuilder() {
+            super(AnnotationTarget.Kind.FIELD);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current field}
+         * has given {@code name} and is declared on given {@code clazz}.
+         *
+         * @param clazz the class, must not be {@code null}
+         * @param name the field name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public FieldBuilder whenField(Class<?> clazz, String name) {
+            Objects.requireNonNull(clazz);
+            return whenField(DotName.createSimple(clazz), name);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current field}
+         * has given {@code name} and is declared on given {@code clazz}.
+         *
+         * @param clazz the class name, must not be {@code null}
+         * @param name the field name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public FieldBuilder whenField(DotName clazz, String name) {
+            Objects.requireNonNull(clazz);
+            Objects.requireNonNull(name);
+            return whenField(field -> field.name().equals(name) && field.declaringClass().name().equals(clazz));
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current field}
+         * matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public FieldBuilder whenField(Predicate<FieldInfo> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> predicate.test(ctx.declaration().asField()));
+        }
+    }
+
+    /**
+     * A builder of {@linkplain AnnotationTransformation annotation transformations} for methods.
+     *
+     * @see #whenMethod(Class, String)
+     * @see #whenMethod(DotName, String)
+     * @see #whenMethod(Predicate)
+     * @see Builder
+     */
+    class MethodBuilder extends Builder<MethodBuilder> {
+        MethodBuilder() {
+            super(AnnotationTarget.Kind.METHOD);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current method}
+         * has given {@code name} and is declared on given {@code clazz}.
+         *
+         * @param clazz the class, must not be {@code null}
+         * @param name the method name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public MethodBuilder whenMethod(Class<?> clazz, String name) {
+            Objects.requireNonNull(clazz);
+            return whenMethod(DotName.createSimple(clazz), name);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current method}
+         * has given {@code name} and is declared on given {@code clazz}.
+         *
+         * @param clazz the class name, must not be {@code null}
+         * @param name the method name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public MethodBuilder whenMethod(DotName clazz, String name) {
+            Objects.requireNonNull(clazz);
+            Objects.requireNonNull(name);
+            return whenMethod(method -> method.name().equals(name) && method.declaringClass().name().equals(clazz));
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current method}
+         * matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public MethodBuilder whenMethod(Predicate<MethodInfo> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> predicate.test(ctx.declaration().asMethod()));
+        }
+    }
+
+    /**
+     * A builder of {@linkplain AnnotationTransformation annotation transformations} for method parameters.
+     *
+     * @see #whenMethodParameter(Class, String)
+     * @see #whenMethodParameter(DotName, String)
+     * @see #whenMethodParameter(Predicate)
+     * @see Builder
+     */
+    class MethodParameterBuilder extends Builder<MethodParameterBuilder> {
+        MethodParameterBuilder() {
+            super(AnnotationTarget.Kind.METHOD_PARAMETER);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current method parameter}
+         * belongs to a method with given {@code name} declared on given {@code clazz}.
+         *
+         * @param clazz the class, must not be {@code null}
+         * @param name the method name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public MethodParameterBuilder whenMethodParameter(Class<?> clazz, String name) {
+            Objects.requireNonNull(clazz);
+            return whenMethodParameter(DotName.createSimple(clazz), name);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current method parameter}
+         * belongs to a method with given {@code name} declared on given {@code clazz}.
+         *
+         * @param clazz the class name, must not be {@code null}
+         * @param name the method name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public MethodParameterBuilder whenMethodParameter(DotName clazz, String name) {
+            Objects.requireNonNull(clazz);
+            Objects.requireNonNull(name);
+            return whenMethodParameter(param -> param.method().name().equals(name)
+                    && param.method().declaringClass().name().equals(clazz));
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current method parameter}
+         * matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public MethodParameterBuilder whenMethodParameter(Predicate<MethodParameterInfo> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> predicate.test(ctx.declaration().asMethodParameter()));
+        }
+    }
+
+    /**
+     * A builder of {@linkplain AnnotationTransformation annotation transformations} for record components.
+     *
+     * @see #whenRecordComponent(Class, String)
+     * @see #whenRecordComponent(DotName, String)
+     * @see #whenRecordComponent(Predicate)
+     * @see Builder
+     */
+    class RecordComponentBuilder extends Builder<RecordComponentBuilder> {
+        RecordComponentBuilder() {
+            super(AnnotationTarget.Kind.RECORD_COMPONENT);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current record component}
+         * has given {@code name} and is declared on given {@code clazz}.
+         *
+         * @param clazz the class, must not be {@code null}
+         * @param name the record component name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public RecordComponentBuilder whenRecordComponent(Class<?> clazz, String name) {
+            Objects.requireNonNull(clazz);
+            return whenRecordComponent(DotName.createSimple(clazz), name);
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current record component}
+         * has given {@code name} and is declared on given {@code clazz}.
+         *
+         * @param clazz the class name, must not be {@code null}
+         * @param name the record component name, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public RecordComponentBuilder whenRecordComponent(DotName clazz, String name) {
+            Objects.requireNonNull(clazz);
+            Objects.requireNonNull(name);
+            return whenRecordComponent(component -> component.name().equals(name)
+                    && component.declaringClass().name().equals(clazz));
+        }
+
+        /**
+         * Adds a predicate that tests whether
+         * the {@linkplain TransformationContext#declaration() current record component}
+         * matches given {@code predicate}.
+         *
+         * @param predicate the predicate, must not be {@code null}
+         * @return this builder
+         * @see #when(Predicate)
+         */
+        public RecordComponentBuilder whenRecordComponent(Predicate<RecordComponentInfo> predicate) {
+            Objects.requireNonNull(predicate);
+            return when(ctx -> predicate.test(ctx.declaration().asRecordComponent()));
+        }
+    }
+}
diff --git a/core/src/main/java/org/jboss/jandex/DotName.java b/core/src/main/java/org/jboss/jandex/DotName.java
index 1bc66b8a..04c3e146 100644
--- a/core/src/main/java/org/jboss/jandex/DotName.java
+++ b/core/src/main/java/org/jboss/jandex/DotName.java
@@ -49,6 +49,7 @@ public final class DotName implements Comparable<DotName> {
     public static final DotName ENUM_NAME;
     public static final DotName RECORD_NAME;
     public static final DotName STRING_NAME;
+    public static final DotName INHERITED_NAME;
     public static final DotName REPEATABLE_NAME;
     public static final DotName RETENTION_NAME;
 
@@ -66,6 +67,7 @@ public final class DotName implements Comparable<DotName> {
         ENUM_NAME = new DotName(JAVA_LANG_NAME, "Enum", true, false);
         RECORD_NAME = new DotName(JAVA_LANG_NAME, "Record", true, false);
         STRING_NAME = new DotName(JAVA_LANG_NAME, "String", true, false);
+        INHERITED_NAME = new DotName(JAVA_LANG_ANNOTATION_NAME, "Inherited", true, false);
         REPEATABLE_NAME = new DotName(JAVA_LANG_ANNOTATION_NAME, "Repeatable", true, false);
         RETENTION_NAME = new DotName(DotName.JAVA_LANG_ANNOTATION_NAME, "Retention", true, false);
     }
diff --git a/core/src/main/java/org/jboss/jandex/Index.java b/core/src/main/java/org/jboss/jandex/Index.java
index b76517bb..2ef6b2c4 100644
--- a/core/src/main/java/org/jboss/jandex/Index.java
+++ b/core/src/main/java/org/jboss/jandex/Index.java
@@ -308,6 +308,8 @@ public static ClassInfo singleClass(InputStream classData) throws IOException {
         return index.getKnownClasses().iterator().next();
     }
 
+    // ---
+
     /**
      * {@inheritDoc}
      */
diff --git a/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java
new file mode 100644
index 00000000..bbfd2ad7
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java
@@ -0,0 +1,128 @@
+package org.jboss.jandex;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * An {@link AnnotationOverlay} that can be freely mutated. The {@link #freeze()} operation
+ * returns a list of {@linkplain AnnotationTransformation annotation transformations} that
+ * can later be used to create an equivalent immutable annotation overlay.
+ *
+ * @since 3.2.0
+ */
+public interface MutableAnnotationOverlay extends AnnotationOverlay {
+    /**
+     * Returns a new builder for a mutable annotation overlay for given {@code index}.
+     *
+     * <p>
+     * <strong>Thread safety</strong>
+     *
+     * <p>
+     * The object returned by the builder is <em>not</em> thread safe and should be confined to a single thread.
+     * After calling {@link #freeze()}, the object becomes immutable and can be shared between threads.
+     *
+     * @param index the Jandex index, must not be {@code null}
+     * @return the mutable annotation overlay builder, never {@code null}
+     */
+    static MutableAnnotationOverlay.Builder builder(IndexView index) {
+        Objects.requireNonNull(index);
+        return new Builder(index);
+    }
+
+    /**
+     * Adds given annotation instance to given {@code declaration}. When asking this annotation
+     * overlay about annotation information for given declaration, the results will include
+     * given annotation instance.
+     *
+     * @param declaration the declaration to modify, must not be {@code null}
+     * @param annotation the annotation instance to add to {@code declaration} for, must not be {@code null}
+     */
+    void addAnnotation(Declaration declaration, AnnotationInstance annotation);
+
+    /**
+     * Removes all annotations matching given {@code predicate} from given {@code declaration}.
+     * When asking this annotation overlay about annotation information for given declaration,
+     * the results will not include matching annotation instances.
+     *
+     * @param declaration the declaration to modify, must not be {@code null}
+     * @param predicate the annotation predicate, must not be {@code null}
+     */
+    void removeAnnotations(Declaration declaration, Predicate<AnnotationInstance> predicate);
+
+    /**
+     * Freezes this mutable annotation overlay and returns the annotation transformations to create
+     * an equivalent immutable annotation overlay. After freezing, the {@link #addAnnotation(Declaration, AnnotationInstance)}
+     * and {@link #removeAnnotations(Declaration, Predicate)} methods will throw an exception.
+     *
+     * @return immutable list of annotation transformations equivalent to mutations performed on this annotation overlay,
+     *         never {@code null}
+     */
+    List<AnnotationTransformation> freeze();
+
+    /**
+     * The builder for a mutable annotation overlay.
+     */
+    final class Builder {
+        private final IndexView index;
+
+        private boolean compatibleMode;
+        private boolean runtimeAnnotationsOnly;
+        private boolean inheritedAnnotations;
+
+        Builder(IndexView index) {
+            this.index = index;
+        }
+
+        /**
+         * When called, the built annotation overlay shall treat method parameters as part of methods.
+         * This means that annotations on method parameters are returned when asking for annotations
+         * of a method, asking for annotations on method parameters results in an exception, and
+         * annotation transformations for methods are produced when adding/removing annotations
+         * to/from a method parameter.
+         * <p>
+         * This method is called {@code compatibleMode} because the built annotation overlay is
+         * compatible with the previous implementation of the same concept in Quarkus.
+         *
+         * @return this builder
+         */
+        public Builder compatibleMode() {
+            compatibleMode = true;
+            return this;
+        }
+
+        /**
+         * When called, the built annotation overlay shall only return runtime-retained annotations;
+         * class-retained annotations are ignored. Note that this only applies to annotations present
+         * in class files (and therefore in Jandex); annotations added to the overlay using
+         * {@link #addAnnotation(Declaration, AnnotationInstance)} are not inspected and are always
+         * returned.
+         *
+         * @return this builder
+         */
+        public Builder runtimeAnnotationsOnly() {
+            runtimeAnnotationsOnly = true;
+            return this;
+        }
+
+        /**
+         * When called, the built annotation overlay shall return {@linkplain java.lang.annotation.Inherited inherited}
+         * annotations per the Java rules.
+         *
+         * @return this builder
+         */
+        public Builder inheritedAnnotations() {
+            inheritedAnnotations = true;
+            return this;
+        }
+
+        /**
+         * Builds and returns a mutable annotation overlay based on the configuration of this builder.
+         *
+         * @return the mutable annotation overlay, never {@code null}
+         */
+        public MutableAnnotationOverlay build() {
+            return new MutableAnnotationOverlayImpl(index, compatibleMode, runtimeAnnotationsOnly, inheritedAnnotations);
+        }
+    }
+}
diff --git a/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlayImpl.java b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlayImpl.java
new file mode 100644
index 00000000..e53b9f44
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlayImpl.java
@@ -0,0 +1,134 @@
+package org.jboss.jandex;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+final class MutableAnnotationOverlayImpl extends AnnotationOverlayImpl implements MutableAnnotationOverlay {
+    private volatile boolean frozen;
+
+    MutableAnnotationOverlayImpl(IndexView index, boolean compatibleMode, boolean runtimeAnnotationsOnly,
+            boolean inheritedAnnotations) {
+        super(index, compatibleMode, runtimeAnnotationsOnly, inheritedAnnotations, Collections.emptyList());
+    }
+
+    @Override
+    Set<AnnotationInstance> getAnnotationsFor(Declaration declaration) {
+        EquivalenceKey key = EquivalenceKey.of(declaration);
+        Set<AnnotationInstance> annotations = overlay.get(key);
+        if (annotations == null) {
+            annotations = getOriginalAnnotations(declaration);
+            overlay.put(key, annotations);
+        }
+        return annotations;
+    }
+
+    @Override
+    public void addAnnotation(Declaration declaration, AnnotationInstance annotation) {
+        if (frozen) {
+            throw new IllegalStateException("Mutable annotation overlay is already frozen");
+        }
+
+        if (annotation.target() == null) {
+            annotation = AnnotationInstance.create(annotation, declaration);
+        }
+
+        getAnnotationsFor(declaration).add(annotation);
+        transformations.add(addTransformation(declaration, annotation));
+    }
+
+    private AnnotationTransformation addTransformation(Declaration declaration, AnnotationInstance annotation) {
+        AnnotationTarget.Kind declarationKind;
+        EquivalenceKey key;
+
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            // the `annotation` has correct `target`, see `addAnnotation()` above,
+            // so we don't need to do anything else
+            declarationKind = AnnotationTarget.Kind.METHOD;
+            key = EquivalenceKey.of(declaration.asMethodParameter().method());
+        } else {
+            declarationKind = declaration.kind();
+            key = EquivalenceKey.of(declaration);
+        }
+
+        return new AnnotationTransformation() {
+            @Override
+            public boolean supports(AnnotationTarget.Kind kind) {
+                return kind == declarationKind;
+            }
+
+            @Override
+            public void apply(TransformationContext context) {
+                if (key.equals(EquivalenceKey.of(context.declaration()))) {
+                    context.add(annotation);
+                }
+            }
+
+            @Override
+            public boolean requiresCompatibleMode() {
+                return compatibleMode;
+            }
+        };
+    }
+
+    @Override
+    public void removeAnnotations(Declaration declaration, Predicate<AnnotationInstance> predicate) {
+        if (frozen) {
+            throw new IllegalStateException("Mutable annotation overlay is already frozen");
+        }
+
+        getAnnotationsFor(declaration).removeIf(predicate);
+        transformations.add(removeTransformation(declaration, predicate));
+    }
+
+    private AnnotationTransformation removeTransformation(Declaration declaration, Predicate<AnnotationInstance> predicate) {
+        AnnotationTarget.Kind declarationKind;
+        EquivalenceKey key;
+        Predicate<AnnotationInstance> finalPredicate;
+
+        if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
+            declarationKind = AnnotationTarget.Kind.METHOD;
+            key = EquivalenceKey.of(declaration.asMethodParameter().method());
+            int position = declaration.asMethodParameter().position();
+            finalPredicate = new Predicate<AnnotationInstance>() {
+                @Override
+                public boolean test(AnnotationInstance annotation) {
+                    return annotation.target() != null
+                            && annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER
+                            && annotation.target().asMethodParameter().position() == position
+                            && predicate.test(annotation);
+                }
+            };
+        } else {
+            declarationKind = declaration.kind();
+            key = EquivalenceKey.of(declaration);
+            finalPredicate = predicate;
+        }
+
+        return new AnnotationTransformation() {
+            @Override
+            public boolean supports(AnnotationTarget.Kind kind) {
+                return kind == declarationKind;
+            }
+
+            @Override
+            public void apply(TransformationContext context) {
+                if (key.equals(EquivalenceKey.of(context.declaration()))) {
+                    context.remove(finalPredicate);
+                }
+            }
+
+            @Override
+            public boolean requiresCompatibleMode() {
+                return compatibleMode;
+            }
+        };
+    }
+
+    @Override
+    public List<AnnotationTransformation> freeze() {
+        frozen = true;
+        return Collections.unmodifiableList(transformations);
+    }
+}
diff --git a/core/src/test/java/org/jboss/jandex/test/AnnotationOverlayTest.java b/core/src/test/java/org/jboss/jandex/test/AnnotationOverlayTest.java
new file mode 100644
index 00000000..ca0602ff
--- /dev/null
+++ b/core/src/test/java/org/jboss/jandex/test/AnnotationOverlayTest.java
@@ -0,0 +1,323 @@
+package org.jboss.jandex.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationOverlay;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.AnnotationTransformation;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.Declaration;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.MethodParameterInfo;
+import org.junit.jupiter.api.Test;
+
+public class AnnotationOverlayTest {
+    @Retention(RetentionPolicy.CLASS)
+    @interface MyClassRetainedAnnotation {
+    }
+
+    @Inherited
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface MyInheritedAnnotation {
+        String value();
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface MyNotInheritedAnnotation {
+        String value();
+    }
+
+    @MyInheritedAnnotation("i")
+    @MyNotInheritedAnnotation("ni")
+    static class AnnotatedSuperClass {
+    }
+
+    @MyAnnotation("c1")
+    @MyRepeatableAnnotation("cr1")
+    @MyRepeatableAnnotation.List({
+            @MyRepeatableAnnotation("cr2"),
+            @MyRepeatableAnnotation("cr3")
+    })
+    @MyClassRetainedAnnotation
+    static class AnnotatedClass extends AnnotatedSuperClass {
+        @MyAnnotation("f1")
+        @MyRepeatableAnnotation("fr1")
+        @MyRepeatableAnnotation.List({
+                @MyRepeatableAnnotation("fr2"),
+                @MyRepeatableAnnotation("fr3")
+        })
+        @MyClassRetainedAnnotation
+        Map<String, List<Number>> field;
+
+        @MyAnnotation("m1")
+        @MyRepeatableAnnotation("mr1")
+        @MyRepeatableAnnotation.List({
+                @MyRepeatableAnnotation("mr2"),
+                @MyRepeatableAnnotation("mr3")
+        })
+        @MyClassRetainedAnnotation
+        void method(@MyAnnotation("m2") @MyClassRetainedAnnotation Map<String, List<Number>> param,
+                @MyAnnotation("m3") @MyClassRetainedAnnotation int[] otherParam) {
+        }
+    }
+
+    @Test
+    public void directTransformation_addAnnotation() throws IOException {
+        AnnotationTransformation transformation = new AnnotationTransformation() {
+            @Override
+            public boolean supports(AnnotationTarget.Kind kind) {
+                return kind == AnnotationTarget.Kind.CLASS;
+            }
+
+            @Override
+            public void apply(TransformationContext context) {
+                if (context.declaration().asClass().name().toString()
+                        .equals("org.jboss.jandex.test.AnnotationOverlayTest$AnnotatedClass")) {
+                    context.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C1").build());
+                }
+            }
+        };
+
+        assertOverlay("c1_C1_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation);
+    }
+
+    @Test
+    public void directTransformation_removeAnnotation() throws IOException {
+        AnnotationTransformation transformation = new AnnotationTransformation() {
+            @Override
+            public boolean supports(AnnotationTarget.Kind kind) {
+                return kind == AnnotationTarget.Kind.CLASS;
+            }
+
+            @Override
+            public void apply(TransformationContext context) {
+                if (context.declaration().asClass().name().toString()
+                        .equals("org.jboss.jandex.test.AnnotationOverlayTest$AnnotatedClass")) {
+                    context.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+                }
+            }
+        };
+
+        assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation);
+    }
+
+    @Test
+    public void directTransformation_addAndRemoveAnnotation() throws IOException {
+        AnnotationTransformation transformation = new AnnotationTransformation() {
+            @Override
+            public boolean supports(AnnotationTarget.Kind kind) {
+                return kind == AnnotationTarget.Kind.CLASS;
+            }
+
+            @Override
+            public void apply(TransformationContext context) {
+                if (context.declaration().asClass().name().toString()
+                        .equals("org.jboss.jandex.test.AnnotationOverlayTest$AnnotatedClass")) {
+                    context.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+                    context.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C2").build());
+                }
+            }
+        };
+
+        assertOverlay("C2_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation);
+    }
+
+    @Test
+    public void builtTransformation_addAnnotation() throws IOException {
+        AnnotationTransformation transformation = AnnotationTransformation.forClasses()
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .transform(ctx -> {
+                    ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C3").build());
+                });
+
+        assertOverlay("c1_C3_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation);
+    }
+
+    @Test
+    public void builtTransformation_removeAnnotation() throws IOException {
+        AnnotationTransformation transformation = AnnotationTransformation.forClasses()
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .transform(ctx -> {
+                    ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+                });
+
+        assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation);
+    }
+
+    @Test
+    public void builtTransformation_addAndRemoveAnnotation() throws IOException {
+        AnnotationTransformation transformation = AnnotationTransformation.forClasses()
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .transform(ctx -> {
+                    ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+                    ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C4").build());
+                });
+
+        assertOverlay("C4_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation);
+    }
+
+    @Test
+    public void multipleNonconflictingTransformations() throws IOException {
+        AnnotationTransformation transformation1 = AnnotationTransformation.forClasses()
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .transform(ctx -> {
+                    ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C5").build());
+                });
+        AnnotationTransformation transformation2 = AnnotationTransformation.forMethods()
+                .whenMethod(DotName.createSimple(AnnotatedClass.class), "method")
+                .transform(ctx -> {
+                    ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("M1").build());
+                });
+
+        assertOverlay("c1_C5_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_M1_mr1_mr2_mr3_m2_m3", transformation1, transformation2);
+    }
+
+    @Test
+    public void multipleConflictingTransformations_firstBeforeSecond() throws IOException {
+        AnnotationTransformation transformation1 = AnnotationTransformation.forClasses()
+                .priority(10)
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .transform(ctx -> {
+                    ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+                });
+        AnnotationTransformation transformation2 = AnnotationTransformation.forClasses()
+                .priority(1)
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .whenAnyMatch(MyAnnotation.DOT_NAME)
+                .transform(ctx -> {
+                    ctx.add(AnnotationInstance.builder(MyOtherAnnotation.DOT_NAME).value("C6").build());
+                });
+
+        assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation1, transformation2);
+    }
+
+    @Test
+    public void multipleConflictingTransformations_secondBeforeFirst() throws IOException {
+        AnnotationTransformation transformation1 = AnnotationTransformation.forClasses()
+                .priority(1)
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .transform(ctx -> {
+                    ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+                });
+        AnnotationTransformation transformation2 = AnnotationTransformation.forClasses()
+                .priority(10)
+                .whenClass(DotName.createSimple(AnnotatedClass.class))
+                .whenAnyMatch(MyAnnotation.DOT_NAME)
+                .transform(ctx -> {
+                    ctx.add(AnnotationInstance.builder(MyOtherAnnotation.DOT_NAME).value("C7").build());
+                });
+
+        assertOverlay("C7_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation1, transformation2);
+    }
+
+    private void assertOverlay(String expectedValues, AnnotationTransformation... transformations) throws IOException {
+        Index index = Index.of(AnnotatedSuperClass.class, AnnotatedClass.class, MyAnnotation.class,
+                MyOtherAnnotation.class, MyRepeatableAnnotation.class, MyRepeatableAnnotation.List.class,
+                MyClassRetainedAnnotation.class, MyInheritedAnnotation.class, MyNotInheritedAnnotation.class);
+
+        for (boolean inheritedAnnotations : Arrays.asList(true, false)) {
+            for (boolean runtimeAnnotationsOnly : Arrays.asList(true, false)) {
+                AnnotationOverlay.Builder builder = AnnotationOverlay.builder(index, Arrays.asList(transformations));
+                if (inheritedAnnotations) {
+                    builder.inheritedAnnotations();
+                }
+                if (runtimeAnnotationsOnly) {
+                    builder.runtimeAnnotationsOnly();
+                }
+                AnnotationOverlay overlay = builder.build();
+
+                StringBuilder values = new StringBuilder();
+
+                ClassInfo clazz = index.getClassByName(AnnotatedClass.class);
+                assertNotNull(clazz);
+
+                assertFalse(overlay.hasAnnotation(clazz, MyNotInheritedAnnotation.class));
+                assertNull(overlay.annotation(clazz, MyNotInheritedAnnotation.class));
+                assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyNotInheritedAnnotation.class).size());
+
+                if (inheritedAnnotations) {
+                    assertTrue(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class));
+                    assertNotNull(overlay.annotation(clazz, MyInheritedAnnotation.class));
+                    assertEquals("i", overlay.annotation(clazz, MyInheritedAnnotation.class).value().asString());
+                    assertEquals(1, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size());
+                } else {
+                    assertFalse(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class));
+                    assertNull(overlay.annotation(clazz, MyInheritedAnnotation.class));
+                    assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size());
+                }
+
+                FieldInfo field = clazz.field("field");
+                assertNotNull(field);
+
+                MethodInfo method = clazz.firstMethod("method");
+                assertNotNull(method);
+
+                MethodParameterInfo parameter1 = method.parameters().get(0);
+                assertNotNull(parameter1);
+
+                MethodParameterInfo parameter2 = method.parameters().get(1);
+                assertNotNull(parameter2);
+
+                for (Declaration declaration : Arrays.asList(clazz, field, method, parameter1, parameter2)) {
+                    if (overlay.hasAnnotation(declaration, MyAnnotation.DOT_NAME)) {
+                        values.append(overlay.annotation(declaration, MyAnnotation.DOT_NAME).value().asString()).append("_");
+                    }
+                    if (overlay.hasAnnotation(declaration, MyOtherAnnotation.DOT_NAME)) {
+                        values.append(overlay.annotation(declaration, MyOtherAnnotation.DOT_NAME).value().asString())
+                                .append("_");
+                    }
+                    if (declaration != method) {
+                        if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.DOT_NAME)) {
+                            values.append(overlay.annotation(declaration, MyRepeatableAnnotation.DOT_NAME).value().asString())
+                                    .append("_");
+                        }
+                        if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.List.DOT_NAME)) {
+                            AnnotationInstance annotation = overlay.annotation(declaration,
+                                    MyRepeatableAnnotation.List.DOT_NAME);
+                            for (AnnotationInstance nestedAnnotation : annotation.value().asNestedArray()) {
+                                values.append(nestedAnnotation.value().asString()).append("_");
+                            }
+                        }
+                    } else { // just to test `annotationsWithRepeatable`, no other reason
+                        for (AnnotationInstance annotation : overlay.annotationsWithRepeatable(declaration,
+                                MyRepeatableAnnotation.DOT_NAME)) {
+                            values.append(annotation.value().asString()).append("_");
+                        }
+                    }
+
+                    if (runtimeAnnotationsOnly) {
+                        assertFalse(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class));
+                        assertNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class));
+                        assertEquals(0, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size());
+                    } else {
+                        assertTrue(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class));
+                        assertNotNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class));
+                        assertEquals(1, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size());
+                    }
+                }
+
+                if (values.length() > 0) {
+                    values.deleteCharAt(values.length() - 1);
+                }
+
+                assertEquals(expectedValues, values.toString());
+            }
+        }
+    }
+}
diff --git a/core/src/test/java/org/jboss/jandex/test/MutableAnnotationOverlayTest.java b/core/src/test/java/org/jboss/jandex/test/MutableAnnotationOverlayTest.java
new file mode 100644
index 00000000..8e90c53d
--- /dev/null
+++ b/core/src/test/java/org/jboss/jandex/test/MutableAnnotationOverlayTest.java
@@ -0,0 +1,201 @@
+package org.jboss.jandex.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.Declaration;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.MethodParameterInfo;
+import org.jboss.jandex.MutableAnnotationOverlay;
+import org.junit.jupiter.api.Test;
+
+public class MutableAnnotationOverlayTest {
+    @Retention(RetentionPolicy.CLASS)
+    @interface MyClassRetainedAnnotation {
+    }
+
+    @Inherited
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface MyInheritedAnnotation {
+        String value();
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface MyNotInheritedAnnotation {
+        String value();
+    }
+
+    @MyInheritedAnnotation("i")
+    @MyNotInheritedAnnotation("ni")
+    static class AnnotatedSuperClass {
+    }
+
+    @MyAnnotation("c1")
+    @MyRepeatableAnnotation("cr1")
+    @MyRepeatableAnnotation.List({
+            @MyRepeatableAnnotation("cr2"),
+            @MyRepeatableAnnotation("cr3")
+    })
+    @MyClassRetainedAnnotation
+    static class AnnotatedClass extends AnnotatedSuperClass {
+        @MyAnnotation("f1")
+        @MyRepeatableAnnotation("fr1")
+        @MyRepeatableAnnotation.List({
+                @MyRepeatableAnnotation("fr2"),
+                @MyRepeatableAnnotation("fr3")
+        })
+        @MyClassRetainedAnnotation
+        Map<String, List<Number>> field;
+
+        @MyAnnotation("m1")
+        @MyRepeatableAnnotation("mr1")
+        @MyRepeatableAnnotation.List({
+                @MyRepeatableAnnotation("mr2"),
+                @MyRepeatableAnnotation("mr3")
+        })
+        @MyClassRetainedAnnotation
+        void method(@MyAnnotation("m2") @MyClassRetainedAnnotation Map<String, List<Number>> param,
+                @MyAnnotation("m3") @MyClassRetainedAnnotation int[] otherParam) {
+        }
+    }
+
+    @Test
+    public void addAnnotation() throws IOException {
+        assertOverlay("c1_C1_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", (index, overlay) -> {
+            ClassInfo clazz = index.getClassByName("org.jboss.jandex.test.MutableAnnotationOverlayTest$AnnotatedClass");
+            overlay.addAnnotation(clazz, AnnotationInstance.builder(MyOtherAnnotation.class).value("C1").build());
+        });
+    }
+
+    @Test
+    public void removeAnnotation() throws IOException {
+        assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", (index, overlay) -> {
+            ClassInfo clazz = index.getClassByName("org.jboss.jandex.test.MutableAnnotationOverlayTest$AnnotatedClass");
+            overlay.removeAnnotations(clazz, annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+        });
+    }
+
+    @Test
+    public void addAndRemoveAnnotation() throws IOException {
+        assertOverlay("C2_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", (index, overlay) -> {
+            ClassInfo clazz = index.getClassByName("org.jboss.jandex.test.MutableAnnotationOverlayTest$AnnotatedClass");
+            overlay.removeAnnotations(clazz, annotation -> annotation.name().equals(MyAnnotation.DOT_NAME));
+            overlay.addAnnotation(clazz, AnnotationInstance.builder(MyOtherAnnotation.class).value("C2").build());
+        });
+    }
+
+    private void assertOverlay(String expectedValues, BiConsumer<IndexView, MutableAnnotationOverlay> action)
+            throws IOException {
+        Index index = Index.of(AnnotatedSuperClass.class, AnnotatedClass.class, MyAnnotation.class,
+                MyOtherAnnotation.class, MyRepeatableAnnotation.class, MyRepeatableAnnotation.List.class,
+                MyClassRetainedAnnotation.class, MyInheritedAnnotation.class, MyNotInheritedAnnotation.class);
+
+        for (boolean inheritedAnnotations : Arrays.asList(true, false)) {
+            for (boolean runtimeAnnotationsOnly : Arrays.asList(true, false)) {
+                MutableAnnotationOverlay.Builder builder = MutableAnnotationOverlay.builder(index);
+                if (inheritedAnnotations) {
+                    builder.inheritedAnnotations();
+                }
+                if (runtimeAnnotationsOnly) {
+                    builder.runtimeAnnotationsOnly();
+                }
+                MutableAnnotationOverlay overlay = builder.build();
+
+                action.accept(index, overlay);
+
+                StringBuilder values = new StringBuilder();
+
+                ClassInfo clazz = index.getClassByName(AnnotatedClass.class);
+                assertNotNull(clazz);
+
+                assertFalse(overlay.hasAnnotation(clazz, MyNotInheritedAnnotation.class));
+                assertNull(overlay.annotation(clazz, MyNotInheritedAnnotation.class));
+                assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyNotInheritedAnnotation.class).size());
+
+                if (inheritedAnnotations) {
+                    assertTrue(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class));
+                    assertNotNull(overlay.annotation(clazz, MyInheritedAnnotation.class));
+                    assertEquals("i", overlay.annotation(clazz, MyInheritedAnnotation.class).value().asString());
+                    assertEquals(1, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size());
+                } else {
+                    assertFalse(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class));
+                    assertNull(overlay.annotation(clazz, MyInheritedAnnotation.class));
+                    assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size());
+                }
+
+                FieldInfo field = clazz.field("field");
+                assertNotNull(field);
+
+                MethodInfo method = clazz.firstMethod("method");
+                assertNotNull(method);
+
+                MethodParameterInfo parameter1 = method.parameters().get(0);
+                assertNotNull(parameter1);
+
+                MethodParameterInfo parameter2 = method.parameters().get(1);
+                assertNotNull(parameter2);
+
+                for (Declaration declaration : Arrays.asList(clazz, field, method, parameter1, parameter2)) {
+                    if (overlay.hasAnnotation(declaration, MyAnnotation.DOT_NAME)) {
+                        values.append(overlay.annotation(declaration, MyAnnotation.DOT_NAME).value().asString()).append("_");
+                    }
+                    if (overlay.hasAnnotation(declaration, MyOtherAnnotation.DOT_NAME)) {
+                        values.append(overlay.annotation(declaration, MyOtherAnnotation.DOT_NAME).value().asString())
+                                .append("_");
+                    }
+                    if (declaration != method) {
+                        if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.DOT_NAME)) {
+                            values.append(overlay.annotation(declaration, MyRepeatableAnnotation.DOT_NAME).value().asString())
+                                    .append("_");
+                        }
+                        if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.List.DOT_NAME)) {
+                            AnnotationInstance annotation = overlay.annotation(declaration,
+                                    MyRepeatableAnnotation.List.DOT_NAME);
+                            for (AnnotationInstance nestedAnnotation : annotation.value().asNestedArray()) {
+                                values.append(nestedAnnotation.value().asString()).append("_");
+                            }
+                        }
+                    } else { // just to test `annotationsWithRepeatable`, no other reason
+                        for (AnnotationInstance annotation : overlay.annotationsWithRepeatable(declaration,
+                                MyRepeatableAnnotation.DOT_NAME)) {
+                            values.append(annotation.value().asString()).append("_");
+                        }
+                    }
+
+                    if (runtimeAnnotationsOnly) {
+                        assertFalse(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class));
+                        assertNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class));
+                        assertEquals(0, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size());
+                    } else {
+                        assertTrue(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class));
+                        assertNotNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class));
+                        assertEquals(1, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size());
+                    }
+                }
+
+                if (values.length() > 0) {
+                    values.deleteCharAt(values.length() - 1);
+                }
+
+                assertEquals(expectedValues, values.toString());
+            }
+        }
+    }
+}
diff --git a/core/src/test/java/org/jboss/jandex/test/MyOtherAnnotation.java b/core/src/test/java/org/jboss/jandex/test/MyOtherAnnotation.java
new file mode 100644
index 00000000..135a8397
--- /dev/null
+++ b/core/src/test/java/org/jboss/jandex/test/MyOtherAnnotation.java
@@ -0,0 +1,13 @@
+package org.jboss.jandex.test;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.jboss.jandex.DotName;
+
+@Retention(RetentionPolicy.RUNTIME)
+@interface MyOtherAnnotation {
+    String value();
+
+    DotName DOT_NAME = DotName.createSimple(MyOtherAnnotation.class.getName());
+}