diff --git a/CLAUDE.md b/CLAUDE.md
index d02fde92f..766fa14f6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -296,9 +296,14 @@ Templates are **reusable, declarative change definitions** that enable "no-code
- `ROLLBACK_FIELD` - Payload type for the rollback operation
**Base Class**: `AbstractChangeTemplate` resolves generic types via reflection and provides:
-- Field management: `changeId`, `isTransactional`, `configuration`, `applyPayload`, `rollbackPayload`
+- Field management: `changeId`, `isTransactional`, `configuration`
+- Generic type resolution for APPLY and ROLLBACK payload classes
- Reflective class collection for GraalVM native image support
+**Specialized Classes**:
+- `AbstractSimpleTemplate`: For single-step changes with `setStep()`/`getStep()`
+- `AbstractSteppableTemplate`: For multi-step changes with `setSteps()`/`getSteps()`
+
**Key Files**:
- `core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java`
- `core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java`
diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java
index 4526c8508..708223a70 100644
--- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java
+++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java
@@ -20,10 +20,21 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
+/**
+ * Abstract base class for change templates providing common functionality.
+ *
+ *
This class handles generic type resolution and provides the common fields
+ * needed by all templates: changeId, isTransactional, and configuration.
+ *
+ *
For new templates, extend one of the specialized abstract classes:
+ *
+ *
{@link AbstractSimpleTemplate} - for templates with a single apply/rollback step
+ *
{@link AbstractSteppableTemplate} - for templates with multiple steps
+ *
+ */
public abstract class AbstractChangeTemplate implements ChangeTemplate {
private final Class configurationClass;
@@ -32,18 +43,6 @@ public abstract class AbstractChangeTemplate> stepsPayload;
-
private final Set> reflectiveClasses;
@@ -94,34 +93,6 @@ public void setConfiguration(SHARED_CONFIGURATION_FIELD configuration) {
this.configuration = configuration;
}
- /**
- * @deprecated Use {@link #setStepsPayload(List)} instead. Will be removed in a future release.
- */
- @Deprecated
- @Override
- public void setApplyPayload(APPLY_FIELD applyPayload) {
- this.applyPayload = applyPayload;
- }
-
- /**
- * @deprecated Use {@link #setStepsPayload(List)} instead. Will be removed in a future release.
- */
- @Deprecated
- @Override
- public void setRollbackPayload(ROLLBACK_FIELD rollbackPayload) {
- this.rollbackPayload = rollbackPayload;
- }
-
- @Override
- public void setStepsPayload(List> stepsPayload) {
- this.stepsPayload = stepsPayload;
- }
-
- @Override
- public List> getStepsPayload() {
- return stepsPayload;
- }
-
@Override
public Class getConfigurationClass() {
return configurationClass;
diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java
new file mode 100644
index 000000000..3642a9b35
--- /dev/null
+++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.api.template;
+
+/**
+ * Abstract base class for templates with a single apply/rollback step.
+ *
+ *
Use this class when your template processes a single operation that may have
+ * an optional rollback. The YAML structure for this template type is:
+ *
+ *
The framework will automatically create a {@link TemplateStep} from the
+ * apply/rollback fields in the YAML and inject it via {@link #setStep}.
+ *
+ * @param the type of shared configuration
+ * @param the type of the apply payload
+ * @param the type of the rollback payload
+ */
+public abstract class AbstractSimpleTemplate
+ extends AbstractChangeTemplate {
+
+ protected TemplateStep step;
+
+ public AbstractSimpleTemplate(Class>... additionalReflectiveClass) {
+ super(additionalReflectiveClass);
+ }
+
+ /**
+ * Sets the step containing the apply and optional rollback payloads.
+ *
+ * @param step the template step
+ */
+ public void setStep(TemplateStep step) {
+ this.step = step;
+ }
+
+ /**
+ * Returns the step containing the apply and optional rollback payloads.
+ *
+ * @return the template step, or null if not set
+ */
+ public TemplateStep getStep() {
+ return step;
+ }
+
+ /**
+ * Checks if this template has a step set.
+ *
+ * @return true if a step is set
+ */
+ public boolean hasStep() {
+ return step != null;
+ }
+
+ /**
+ * Convenience method to get the apply payload from the step.
+ *
+ * @return the apply payload, or null if no step is set
+ */
+ public APPLY getApply() {
+ return step != null ? step.getApply() : null;
+ }
+
+ /**
+ * Convenience method to get the rollback payload from the step.
+ *
+ * @return the rollback payload, or null if no step is set or no rollback defined
+ */
+ public ROLLBACK getRollback() {
+ return step != null ? step.getRollback() : null;
+ }
+
+ /**
+ * Checks if this template has a rollback payload defined.
+ *
+ * @return true if a step is set and it has a rollback payload
+ */
+ public boolean hasRollback() {
+ return step != null && step.hasRollback();
+ }
+}
diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java
new file mode 100644
index 000000000..310188031
--- /dev/null
+++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.api.template;
+
+import java.util.List;
+
+/**
+ * Abstract base class for templates with multiple steps.
+ *
+ *
Use this class when your template processes multiple operations, each with
+ * its own apply and optional rollback. The YAML structure for this template type is:
+ *
+ *
The framework will automatically parse the steps from the YAML and inject
+ * them via {@link #setSteps}.
+ *
+ *
Rollback Behavior:
+ *
+ *
When a step fails, all previously successful steps are rolled back in reverse order
+ *
Steps without rollback operations are skipped during rollback
+ *
Rollback errors are logged but don't stop the rollback process
+ *
+ *
+ * @param the type of shared configuration
+ * @param the type of the apply payload for each step
+ * @param the type of the rollback payload for each step
+ */
+public abstract class AbstractSteppableTemplate
+ extends AbstractChangeTemplate {
+
+ protected List> steps;
+
+ public AbstractSteppableTemplate(Class>... additionalReflectiveClass) {
+ super(additionalReflectiveClass);
+ }
+
+ /**
+ * Sets the list of steps to execute.
+ *
+ * @param steps the list of template steps
+ */
+ public void setSteps(List> steps) {
+ this.steps = steps;
+ }
+
+ /**
+ * Returns the list of steps.
+ *
+ * @return the list of template steps, or null if not set
+ */
+ public List> getSteps() {
+ return steps;
+ }
+
+}
diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java
index fe9968f5c..c99e20d1f 100644
--- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java
+++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java
@@ -15,13 +15,17 @@
*/
package io.flamingock.api.template;
-import java.util.List;
-
/**
* Interface representing a reusable change template with configuration of type {@code CONFIG}.
*
*
This interface is commonly implemented by classes that act as templates for Changes
* where a specific configuration needs to be injected and managed independently.
+ *
+ *
Templates should extend one of the abstract base classes:
+ *
+ *
{@link AbstractSimpleTemplate} - for templates with a single apply/rollback step
+ *
{@link AbstractSteppableTemplate} - for templates with multiple steps
+ *
*/
public interface ChangeTemplate extends ReflectionMetadataProvider {
@@ -31,26 +35,6 @@ public interface ChangeTemplate> stepsPayload);
-
- List> getStepsPayload();
-
- default boolean hasStepsPayload() {
- return getStepsPayload() != null && !getStepsPayload().isEmpty();
- }
-
Class getConfigurationClass();
Class getApplyPayloadClass();
diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java
index b556fa170..176b0e5f0 100644
--- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java
+++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java
@@ -15,6 +15,8 @@
*/
package io.flamingock.internal.core.task.executable;
+import io.flamingock.api.template.AbstractSimpleTemplate;
+import io.flamingock.api.template.AbstractSteppableTemplate;
import io.flamingock.api.template.ChangeTemplate;
import io.flamingock.api.template.TemplateStep;
import io.flamingock.internal.common.core.error.ChangeExecutionException;
@@ -51,49 +53,84 @@ protected void executeInternal(ExecutionRuntime executionRuntime, Method method
ChangeTemplate,?,?> changeTemplateInstance = (ChangeTemplate,?,?>) instance;
changeTemplateInstance.setTransactional(descriptor.isTransactional());
changeTemplateInstance.setChangeId(descriptor.getId());
- setExecutionData(executionRuntime, changeTemplateInstance, "Configuration");
- setExecutionData(executionRuntime, changeTemplateInstance, "ApplyPayload");
- setExecutionData(executionRuntime, changeTemplateInstance, "RollbackPayload");
- setStepsIfPresent(executionRuntime, changeTemplateInstance);
+ setConfigurationData(executionRuntime, changeTemplateInstance);
+
+ if (instance instanceof AbstractSteppableTemplate) {
+ setStepsForSteppableTemplate(executionRuntime, (AbstractSteppableTemplate, ?, ?>) instance);
+ } else if (instance instanceof AbstractSimpleTemplate) {
+ setStepForSimpleTemplate(executionRuntime, (AbstractSimpleTemplate, ?, ?>) instance);
+ }
+
executionRuntime.executeMethodWithInjectedDependencies(instance, method);
} catch (Throwable ex) {
throw new ChangeExecutionException(ex.getMessage(), this.getId(), ex);
}
}
+ /**
+ * Sets the steps for an AbstractSteppableTemplate.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private void setStepsForSteppableTemplate(ExecutionRuntime executionRuntime,
+ AbstractSteppableTemplate, ?, ?> instance) {
+ Object stepsData = descriptor.getSteps();
+
+ if (stepsData != null) {
+ logger.debug("Setting steps for steppable template change[{}]", descriptor.getId());
+
+ List convertedSteps = convertToTemplateSteps(
+ stepsData,
+ instance.getApplyPayloadClass(),
+ instance.getRollbackPayloadClass()
+ );
+
+ Method setStepsMethod = getSetterMethod(instance.getClass(), "setSteps");
+ executionRuntime.executeMethodWithParameters(instance, setStepsMethod, convertedSteps);
+ } else {
+ logger.warn("No 'steps' section provided for steppable template-based change[{}]", descriptor.getId());
+ }
+ }
- private void setExecutionData(ExecutionRuntime executionRuntime,
- ChangeTemplate, ?, ?> instance,
- String setterName) {
- Class> parameterClass;
- Object data;
- switch (setterName) {
- case "Configuration":
- parameterClass = instance.getConfigurationClass();
- data = descriptor.getConfiguration();
- break;
- case "ApplyPayload":
- parameterClass = instance.getApplyPayloadClass();
- data = descriptor.getApply();
- break;
- case "RollbackPayload":
- parameterClass = instance.getRollbackPayloadClass();
- data = descriptor.getRollback();
- break;
- default:
- throw new RuntimeException("Not found config setter for template: " + instance.getClass().getSimpleName());
+ /**
+ * Sets the step for an AbstractSimpleTemplate.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private void setStepForSimpleTemplate(ExecutionRuntime executionRuntime,
+ AbstractSimpleTemplate, ?, ?> instance) {
+ Object applyData = descriptor.getApply();
+
+ if (applyData != null) {
+ logger.debug("Setting step for simple template change[{}]", descriptor.getId());
+
+ TemplateStep step = convertToTemplateStep(
+ applyData,
+ descriptor.getRollback(),
+ instance.getApplyPayloadClass(),
+ instance.getRollbackPayloadClass()
+ );
+
+ Method setStepMethod = getSetterMethod(instance.getClass(), "setStep");
+ executionRuntime.executeMethodWithParameters(instance, setStepMethod, step);
+ } else {
+ logger.warn("No 'apply' section provided for simple template-based change[{}]", descriptor.getId());
}
- Method setConfigurationMethod = getSetterMethod(instance.getClass(), "set" + setterName);
+ }
+
+ private void setConfigurationData(ExecutionRuntime executionRuntime,
+ ChangeTemplate, ?, ?> instance) {
+ Class> parameterClass = instance.getConfigurationClass();
+ Object data = descriptor.getConfiguration();
- if(data != null && Void.class != parameterClass) {
+ if (data != null && Void.class != parameterClass) {
+ Method setConfigurationMethod = getSetterMethod(instance.getClass(), "setConfiguration");
executionRuntime.executeMethodWithParameters(
instance,
setConfigurationMethod,
FileUtil.getFromMap(parameterClass, data));
- } else if(Void.class != parameterClass ) {
- logger.warn("No '{}' section provided for template-based change[{}] of type[{}]", setterName, descriptor.getId(), descriptor.getTemplateClass().getName());
+ } else if (Void.class != parameterClass) {
+ logger.warn("No 'Configuration' section provided for template-based change[{}] of type[{}]",
+ descriptor.getId(), descriptor.getTemplateClass().getName());
}
-
}
@@ -107,27 +144,30 @@ private Method getSetterMethod(Class> changeTemplateClass, String methodName)
}
/**
- * Sets the steps on the template if steps data is present.
- * Converts raw step data (List of Maps) to List of TemplateStep using the template's payload classes.
+ * Converts raw apply/rollback data from YAML to a TemplateStep object.
+ *
+ * @param applyData the raw apply data
+ * @param rollbackData the raw rollback data (maybe null)
+ * @param applyClass the class type for apply payload
+ * @param rollbackClass the class type for rollback payload
+ * @return the converted TemplateStep object
*/
@SuppressWarnings({"unchecked", "rawtypes"})
- private void setStepsIfPresent(ExecutionRuntime executionRuntime,
- ChangeTemplate, ?, ?> instance) {
- Object stepsData = descriptor.getSteps();
- if (stepsData == null) {
- return;
+ private TemplateStep convertToTemplateStep(Object applyData,
+ Object rollbackData,
+ Class> applyClass,
+ Class> rollbackClass) {
+ TemplateStep step = new TemplateStep();
+
+ if (applyData != null && Void.class != applyClass) {
+ step.setApply(FileUtil.getFromMap(applyClass, applyData));
}
- logger.debug("Setting steps for change[{}]", descriptor.getId());
-
- List convertedSteps = convertToTemplateSteps(
- stepsData,
- instance.getApplyPayloadClass(),
- instance.getRollbackPayloadClass()
- );
+ if (rollbackData != null && Void.class != rollbackClass) {
+ step.setRollback(FileUtil.getFromMap(rollbackClass, rollbackData));
+ }
- Method setStepsMethod = getSetterMethod(instance.getClass(), "setStepsPayload");
- executionRuntime.executeMethodWithParameters(instance, setStepsMethod, convertedSteps);
+ return step;
}
/**
diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java
index 84d1ac984..ad14e6467 100644
--- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java
+++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java
@@ -15,6 +15,7 @@
*/
package io.flamingock.internal.core.task.executable.builder;
+import io.flamingock.api.template.AbstractSteppableTemplate;
import io.flamingock.internal.common.core.recovery.action.ChangeAction;
import io.flamingock.internal.core.task.executable.ExecutableTask;
import io.flamingock.internal.core.task.executable.TemplateExecutableTask;
@@ -88,7 +89,15 @@ private TemplateExecutableTask buildTask(String stageName,
TemplateLoadedChange loadedTask,
ChangeAction action) {
Method rollbackMethod = null;
- if (loadedTask.getRollback() != null) {
+
+ boolean isSteppableTemplate = AbstractSteppableTemplate.class.isAssignableFrom(loadedTask.getTemplateClass());
+
+ if (isSteppableTemplate) {
+ rollbackMethod = loadedTask.getRollbackMethod().orElse(null);
+ if (rollbackMethod != null) {
+ logger.trace("Change[{}] is a steppable template with rollback method", loadedTask.getId());
+ }
+ } else if (loadedTask.getRollback() != null) {
rollbackMethod = loadedTask.getRollbackMethod().orElse(null);
if (rollbackMethod != null) {
logger.trace("Change[{}] provides rollback in configuration", loadedTask.getId());
@@ -106,6 +115,7 @@ private TemplateExecutableTask buildTask(String stageName,
);
}
}
+
return new TemplateExecutableTask(
stageName,
loadedTask,
@@ -115,4 +125,4 @@ private TemplateExecutableTask buildTask(String stageName,
);
}
-}
\ No newline at end of file
+}
diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java
similarity index 80%
rename from core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java
rename to core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java
index 9fdb5b8b4..7d4bc20d3 100644
--- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java
+++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java
@@ -17,8 +17,7 @@
import io.flamingock.internal.common.core.error.FlamingockException;
import io.flamingock.internal.common.core.template.ChangeTemplateManager;
-import io.flamingock.api.template.ChangeTemplate;
-import io.flamingock.api.template.TemplateStep;
+import io.flamingock.api.template.AbstractSimpleTemplate;
import io.flamingock.api.annotations.Apply;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -26,55 +25,21 @@
import org.mockito.MockedStatic;
import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
-class TemplateLoadedTaskBuilderTest {
+class SimpleTemplateLoadedTaskBuilderTest {
private TemplateLoadedTaskBuilder builder;
- // Simple test template implementation
- public static class TestChangeTemplate implements ChangeTemplate