If a (public) method is declared at a superclass of an @Transactional target class, the method won't be wrapped in a transaction.

The problem lies in AbstractFallbackTransactionAttributeSource:

    protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
        // Don't allow non-public methods, as configured.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
            return null;
        }

        // The method may be on an interface, but we need attributes from the target class.
        // If the target class is null, the method will be unchanged.
                // -> This will resolve to the method at the superclass. This method has no @Transactional.
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

        // First try is the method in the target class.
                // -> This will resolve to the method at the superclass too because the method has not been overwritten.
        TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
        if (txAttr != null) {
            return txAttr;
        }

        // Second try is the transaction attribute on the target class.
                // -> This will resolve to the superclass. The superclass has no @Transactional.
        txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
        if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
            return txAttr;
        }

        // Bug: This third case is missing:
        // -> The target class has @Transactional.
        // Maybe this can be combined with the case above.
        if (targetClass != null) {
            txAttr = findTransactionAttribute(targetClass);
            if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
                return txAttr;
            }
        }

        if (specificMethod != method) {
            // Fallback is to look at the original method.
            txAttr = findTransactionAttribute(method);
            if (txAttr != null) {
                return txAttr;
            }
            // Last fallback is the class of the original method.
            txAttr = findTransactionAttribute(method.getDeclaringClass());
            if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
                return txAttr;
            }
        }

        return null;
    }

Comment From: jhoeller

This is by design: A class-level @Transactional marker is meant to apply as a default to all methods of the declaring class and its subclasses. It does not apply to ancestor classes up the class hierarchy; inherited methods need to be locally redeclared in order to participate in a subclass-level annotation. See the @Transactional javadoc as well as https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html#transaction-declarative-annotations-method-visibility