Skip to content

Mongock migration fails when @ChangeUnit inherits @Execution from a base class #979

Open

Description

@chriptus13

Description

When migrating an existing application from Mongock to Flamingock, Flamingock fails to process a legacy Mongock @ChangeUnit whose @Execution and @RollbackExecution methods are inherited from a base template class.

The existing Mongock audit record contains the correct changeLogClass. However, when Flamingock loads the generated preview, the preview points to the concrete @ChangeUnit class and does not appear to include the inherited execution method.

This results in the following exception:

java.lang.NullPointerException: Cannot invoke "io.flamingock.internal.common.core.preview.PreviewMethod.getName()" because the return value of "io.flamingock.internal.common.core.preview.CodePreviewChange.getApplyPreviewMethod()" is null

Steps to reproduce

  1. Create a Mongock @ChangeUnit as follows:
abstract class MigrationTemplate {

    @Execution
    fun execute() {
        // Shared migration logic
    }

    @RollbackExecution
    fun rollback() {
        // Shared rollback logic
    }
}

@ChangeUnit(
    id = "example-change",
    order = "001",
    author = "team",
)
class ExampleChangeUnit : MigrationTemplate()
  1. Run Mongock migration
  2. Follow Flamingock coming from Mongock guide -> https://docs.flamingock.io/resources/coming-from-mongock
  3. Run Flamingock migration

Expected result

Flamingock imports Mongock changelog history successfully.

Environment Information

  • Mongock version 5.5.1
  • Flamingock version 1.4.5

Activity

  1. chriptus13 commented on Oct 21, 2026

    @chriptus13
    Author

    After some digging I believe the issue is in this method which does not walk its base classes. I would suggest something like follows:

    private Optional<PreviewMethod> getAnnotatedMethodInfo(TypeElement typeElement, Class<? extends Annotation> annotationType) {
        if (typeElement == null) {
            return Optional.empty();
        }
        for (Element enclosedElement : typeElement.getEnclosedElements()) {
            if (enclosedElement.getKind() == ElementKind.METHOD &&
                    enclosedElement.getAnnotation(annotationType) != null) {
    
                ExecutableElement method = (ExecutableElement) enclosedElement;
                return Optional.of(previewMethodFromExecutableElement(method));
            }
        }
    
        TypeMirror superclass = typeElement.getSuperclass();
        if (superclass.getKind() == TypeKind.NONE) {
            return Optional.empty();
        }
    
        Element superclassElement = types.asElement(superclass);
    
        return getAnnotatedMethodInfo(
            superclassElement instanceof TypeElement
                ? (TypeElement) superclassElement
                : null,
            annotationType
        );
    }

    This way we can walk through the base classes also.

  2. bercianor commented on Oct 21, 2026

    @bercianor
    Member

    Thanks for reporting this and investigating the cause, @chriptus13!

    Your proposed approach looks reasonable. Would you be willing to open a PR with the fix? A regression test covering inherited @execution and @RollbackExecution methods would be very helpful. We'll review it asap.

    Thanks!

  3. chriptus13 commented on Oct 22, 2026

    @chriptus13
    Author

    Hey @bercianor, I've managed to find time to open a PR with the fix ✌️
    Moreover, I've noticed that current Flamingock also had the same issue, so I went ahead and did the fix for both.
    Feel free to review it

  4. bercianor commented on Oct 23, 2026

    @bercianor
    Member

    Thanks for taking the time to contribute!

    We're currently reviewing the PR, including the changes for both legacy Mongock and native Flamingock. We'll get back to you once we've completed the review.

  5. bercianor commented on Oct 23, 2026

    @bercianor
    Member

    Regarding native Flamingock, if your goal is to reuse common logic across multiple changes, we'd encourage you to take a look at Flamingock Templates (https://docs.flamingock.io/templates/templates-introduction).

    Templates are specifically designed to encapsulate reusable logic and allow changes to be defined declaratively, without relying on class inheritance. Depending on your use case, they might be a better fit.

    That said, we'd be interested in better understanding your use case. Could you share a concrete example of why you'd want to use this inheritance pattern with native Flamingock changes? This would help us evaluate whether there's a need that isn't adequately covered by Templates.

  6. chriptus13 commented on Oct 23, 2026

    @chriptus13
    Author

    Let me take a look at Flamingock Templates and come back to you ✌️

    Update
    From my understanding this would be to support defining changes in a declarative way (via yaml) right? Cannot be used from a @Change class?
    Our use case is doing what we were with Mongock previously, e.g. for a create collection template:

    open class CreateCollectionTemplate(private val collectionName: String) {
        @Execution
        fun execution(mongoTemplate: MongoTemplate) {
            mongoTemplate.createCollection(collectionName)
        }
    
        @RollbackExecution
        fun rollbackExecution(mongoTemplate: MongoTemplate) {
            mongoTemplate.dropCollection(collectionName)
        }
    }
    
    // And then
    @ChangeUnit(id = "some-id", order = "001", author = "author")
    class V001CreateSimpleCollection: CreateCollectionTemplate("my-collection")
  7. bercianor commented on Oct 23, 2026

    @bercianor
    Member

    Thanks for sharing the example! That helps clarify your use case.

    You're right that Flamingock Templates are YAML-based and cannot be used directly from an @Change class.

    However, what I had in mind was creating your own custom Flamingock Template (https://docs.flamingock.io/templates/create-your-own-template), where you could encapsulate your existing create/drop collection logic and pass the collection name through YAML.

    For example, once you've defined a custom create-collection template, each change could be as simple as:

    id: some-id
    template: create-collection
    targetSystem:
      id: mongodb
    apply: my-collection
    rollback: my-collection

    This would allow you to reuse the same Java/Kotlin implementation across multiple changes, without having to create a new subclass for each collection.

    Of course, we're still happy to support the inheritance pattern in native Flamingock. I just wanted to highlight another approach that might be a good fit for your use case, especially for new changes.

    Does this approach make sense for your scenario, or is there a specific reason you'd prefer to keep defining each change as a Kotlin @Change class?

  8. chriptus13 commented on Oct 23, 2026

    @chriptus13
    Author

    We would prefer to keep defining each change as a @Change class since this way we make sure to have a single source of truth for all the migrations. However, I understand that we could use your approach but that would lead to having @Change classes split between code and resources.

  9. bercianor commented on Oct 23, 2026

    @bercianor
    Member

    That makes perfect sense! We understand your preference for keeping all migrations consistent and code-based.

    Just one small clarification: Flamingock actually supports placing YAML-based changes alongside code-based changes in the same source package (including src/main/kotlin), so you wouldn't need to split them between code and resources. Both types of changes can coexist within the same stage.

    You can find more details in our Setup & Stages documentation (https://docs.flamingock.io/flamingock-library-config/setup-and-stages).

    Of course, that still means mixing two different ways of defining changes, so we completely understand your preference for sticking with @Change classes.

    Thanks for sharing your reasoning!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions