Skip to content

State & Lifecycle

Steppy provides several mechanisms for managing state and sharing data between steps. This includes local state variables, flow-scoped state, and the powerful @Provides/@Consumes pattern for dependency injection.

Local State Variables

The execution context can hold variables that survive between steps. Mark a field with @State and Steppy injects a typed Variable that reads and writes the value.

class StateStep implements Step<None, Integer, Integer> {
    @State
    Variable<Integer> counter;

    @Override
    public Integer invoke(Context<None> ctx, Integer input) {
        Integer current = counter.get(ctx);
        counter.set(ctx, current == null ? input : current + input);
        return input;
    }
}

Variables are by default scoped to the declaring step. Setting @State(scope = Scope.FLOW) shares the variable across all steps in the flow. Marking a variable as readOnly makes it immutable.

Provider/Consumer Pattern

The @Provides and @Consumes annotations enable a powerful dependency injection pattern where steps can provide data that other steps consume. This creates a clean separation of concerns and allows for flexible data flow.

Providing Data

Use @Provides on a Variable<T> field to indicate that a step provides data of type T:

class UserDataProviderStep implements Step<None, None, None> {
    @Provides
    Variable<UserData> userData;

    @Override
    public None invoke(Context<None> context, None input) {
        // Fetch user data from database
        UserData data = fetchUserFromDatabase();
        userData.set(context, data);
        return None.value();
    }
}

Consuming Data

Use @Consumes on a Variable<T> field to indicate that a step requires data of type T:

class UserDataConsumerStep implements Step<None, None, String> {
    @Consumes
    Variable<UserData> userData;

    @Override
    public String invoke(Context<None> context, None input) {
        UserData data = userData.get(context);
        return "Hello, " + data.name() + "!";
    }
}

Building Flows with Providers/Consumers

When building flows, Steppy automatically validates that all consumed data is provided by some step in the flow:

// Register steps and initialize
StaticStepRepository.register(UserDataProviderStep.class, UserDataConsumerStep.class);
StaticFlowBuilderFactory.initialize(Executors.newFixedThreadPool(4));

var flow = StaticFlowBuilderFactory
    .builder(None.class, String.class)
    .append(UserDataProviderStep.class)  // Provides UserData
    .append(UserDataConsumerStep.class)  // Consumes UserData
    .build();

Result<String> result = flow.invoke();

Validation

Steppy performs validation to ensure that all consumed data is provided. If a step consumes data that isn't provided by any step in the flow, a ValidationException is thrown:

// This will throw ValidationException
StaticStepRepository.register(UserDataConsumerStep.class);
StaticFlowBuilderFactory.initialize(Executors.newFixedThreadPool(4));

var invalidFlow = StaticFlowBuilderFactory
    .builder(None.class, String.class)
    .append(UserDataConsumerStep.class)  // Consumes UserData but no provider
    .build();

Nested Flows

The provider/consumer pattern works seamlessly with nested flows. Data provided in a parent flow is available to all nested flows:

StaticStepRepository.register(UserDataProviderStep.class, UserDataConsumerStep.class);
StaticFlowBuilderFactory.initialize(Executors.newFixedThreadPool(4));

var flow = StaticFlowBuilderFactory
    .builder(None.class, String.class)
    .append(UserDataProviderStep.class)
    .nest(None.class, nestedBuilder -> {
        nestedBuilder.append(UserDataConsumerStep.class);
    })
    .build();

Lifecycle hooks

Use @Before and @After on methods to run code at specific times. Hooks may target Scope.STEP or Scope.FLOW and are often used for resource management.

class LifecycleStep implements Step<None, None, None> {
    @Before(Scope.FLOW)
    void open(Context<None> ctx) {
        // allocate resources
    }

    @After(Scope.FLOW)
    void close(Context<None> ctx) {
        // release resources
    }

    @Override
    public None invoke(Context<None> ctx, None in) { return None.value(); }
}