Developer workflow with Fluxtion


Fluxtion has been designed to have a simple on ramp that decreases developer inertia for building event driven logic. A good approach to using Fluxtion for the first time or integrating into an existing project is:

  1. Add the fluxtion-compiler library as a runtime dependency
  2. Use Fluxtion.interpret to experiment and create application logic, events and service interfaces
  3. Refactor the research event handling code ready for production, and call from unit tests
  4. For AOT move fluxtion-compiler to compile only dependency, use maven plugin to generate event processor as part of the build
  5. Use the event processor in your application

Each step builds incrementally allowing progress at the developers chosen pace, there is no big bang requirement to get started with Fluxtion

Table of contents

Example project

In our example we are building an authorized command executor only authorized users can execute a command. The example source project is here.

Processing logic

Our design sketches show what we intend to integrate into our system

flowchart TB
    classDef eventHandler color:#022e1f,fill:#aaa3ff,stroke:#000;
    classDef graphNode color:#022e1f,fill:#00cfff,stroke:#000;
    classDef exportedService color:#022e1f,fill:#aaa3ff,stroke:#000;
    style EventProcessor fill:#e9ebe4,stroke:#333,stroke-width:1px

    AuthorizedCall><b>ServiceCall</b> authorize:CommandPermission]:::eventHandler
    RemoveAuthorizedCall><b>ServiceCall</b> removeAuthorized:CommandPermission]:::eventHandler
    
    AdminCommand><b>InputEvent</b>::AdminCommand]:::eventHandler
    CommandAuthorizer([<b>ServiceLookup</b>::CommandAuthorizer]):::exportedService

    CommandExecutor[CommandExecutor\n<b>EventHandler</b>::AdminCommand]:::graphNode
    CommandAuthorizerNode[CommandAuthorizerNode\n <b>ExportService</b>::CommandAuthorizer]:::graphNode

    AdminCommand --> CommandExecutor
    AuthorizedCall & RemoveAuthorizedCall --> CommandAuthorizer --> CommandAuthorizerNode
 
    subgraph EventProcessor
        CommandAuthorizerNode --> CommandExecutor
    end

1 - Add fluxtion-compiler build dependency

Maven build file for getting started with Fluxtion-compiler as a runtime dependency. Fluxtion.interpret requires the compiler library at runtime.

    <dependencies>
        <dependency>
            <groupId>com.fluxtion</groupId>
            <artifactId>runtime</artifactId>
            <version>9.3.8</version>
        </dependency>
        <dependency>
            <groupId>com.fluxtion</groupId>
            <artifactId>compiler</artifactId>
            <version>9.3.8</version>
        </dependency>
    </dependencies>
implementation 'com.fluxtion:runtime:9.3.8'
implementation 'com.fluxtion:compiler:9.3.8'
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>example.master</artifactId>
        <groupId>com.fluxtion.example</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <name>getting-started :: developer-workflow</name>
    <artifactId>developer-workflow</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.fluxtion</groupId>
            <artifactId>compiler</artifactId>
            <version>9.3.8</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.1.0-jre</version>
        </dependency>
    </dependencies>
</project>

2 - Experiment with app solutions

One approach is to add all the classes as nested classes into a experiment class with a main method. Use Fluxtion.interpret to construct the processor and fire events into it or make service calls.

For our research we are experimenting with:

  • Records as events
  • Guava multimap as a permission map
  • Adding/removing permissions via an api
  • Using a main method to construct the processor with Fluxtion.interpret
  • Adding debug methods, printing to System.out for logging

Code sample

The quick development cycle allows the developer to experiment and develop with very little friction, see MainExperiment

public class MainExperiment {
    public static void main(String[] args) {
        var processor = Fluxtion.interpret(new CommandExecutor(new CommandAuthorizerNode()));
        processor.init();

        CommandAuthorizer commandAuthorizer = processor.getExportedService();
        commandAuthorizer.authorize(new CommandPermission("admin", "shutdown"));
        commandAuthorizer.authorize(new CommandPermission("admin", "listUser"));
        commandAuthorizer.authorize(new CommandPermission("Aslam", "listUser"));
        commandAuthorizer.authorize(new CommandPermission("Puck", "createMischief"));

        commandAuthorizer.dumpMap();

        processor.onEvent(new AdminCommand("admin", "shutdown", () -> System.out.println("executing shutdown command")));
        processor.onEvent(new AdminCommand("Aslam", "listUser", () -> System.out.println("executing listUser command")));
        processor.onEvent(new AdminCommand("Puck", "createMischief", () -> System.out.println("move the stool")));
        processor.onEvent(new AdminCommand("Aslam", "shutdown", () -> System.out.println("executing shutdown command")));

        commandAuthorizer.removeAuthorized(new CommandPermission("Puck", "createMischief"));
        commandAuthorizer.dumpMap();
        processor.onEvent(new AdminCommand("Puck", "createMischief", () -> System.out.println("move the stool")));
    }
    
    public interface CommandAuthorizer {
        boolean authorize(CommandPermission commandPermission);
        boolean removeAuthorized(CommandPermission commandPermission);
        //used for testing
        void dumpMap();
    }

    public static class CommandAuthorizerNode implements @ExportService CommandAuthorizer {
        private transient final Multimap<String, String> permissionMap = HashMultimap.create();

        @Override
        public boolean authorize(CommandPermission commandPermission) {
            permissionMap.put(commandPermission.user, commandPermission.command);
            return false;
        }

        @Override
        public boolean removeAuthorized(CommandPermission commandPermission) {
            permissionMap.remove(commandPermission.user, commandPermission.command);
            return false;
        }

        @Override
        public void dumpMap() {
            System.out.println("""
                    
                    Permission map
                    --------------------
                    %s
                    --------------------
                    """.formatted(permissionMap.toString()));
        }

        boolean isAuthorized(AdminCommand adminCommand) {
            return permissionMap.containsEntry(adminCommand.user, adminCommand.command);
        }
    }

    @Data
    public static class CommandExecutor {
        @NoTriggerReference
        private final CommandAuthorizerNode commandAuthorizer;

        @OnEventHandler
        public boolean executeCommand(AdminCommand command) {
            boolean authorized = commandAuthorizer.isAuthorized(command);
            if (authorized) {
                System.out.println("Executing command " + command);
                command.commandToExecute().run();
            } else {
                System.out.println("FAILED authorization for command " + command);
            }
            return authorized;
        }
    }

    public record CommandPermission(String user, String command) { }

    public record AdminCommand(String user, String command, Runnable commandToExecute) {
        @Override
        public String toString() {
            return "AdminCommand{user='" + user + '\'' + ", command='" + command + '\'' +'}';
        }
    }
}

Experiment execution output

Permission map
--------------------
{admin=[listUser, shutdown], Puck=[createMischief], Aslam=[listUser]}
--------------------

Executing command AdminCommand{user='admin', command='shutdown'}
executing shutdown command
Executing command AdminCommand{user='Aslam', command='listUser'}
executing listUser command
Executing command AdminCommand{user='Puck', command='createMischief'}
move the stool
FAILED authorization for command AdminCommand{user='Aslam', command='shutdown'}

Permission map
--------------------
{admin=[listUser, shutdown], Aslam=[listUser]}
--------------------

FAILED authorization for command AdminCommand{user='Puck', command='createMischief'}

3 - Integrate and unit test

We are happy with the direction of the development so we move to integrating the event processor into our application.

  • Move all our nested classes to top level classes. Delete the Experiment class with the test main method
  • Remove debug methods and any debug print statements
  • Create a unit test that validates our desired behaviour

The experiment is refactored into the integrating package

Unit test

The sample unit test demonstrates how our solution now meets the application requirements

class CommandExecutorTest {
    @Test
    public void testPermission(){
        var processor = Fluxtion.interpret(new CommandExecutor(new CommandAuthorizerNode()));
        processor.init();

        CommandAuthorizer commandAuthorizer = processor.getExportedService();
        commandAuthorizer.authorize(new CommandPermission("admin", "shutdown"));
        commandAuthorizer.authorize(new CommandPermission("admin", "listUser"));
        commandAuthorizer.authorize(new CommandPermission("Aslam", "listUser"));
        commandAuthorizer.authorize(new CommandPermission("Puck", "createMischief"));

        LongAdder longAdder = new LongAdder();
        processor.onEvent(new AdminCommand("admin", "shutdown", longAdder::increment));
        Assertions.assertEquals(1, longAdder.intValue());

        processor.onEvent(new AdminCommand("Aslam", "listUser", longAdder::increment));
        Assertions.assertEquals(2, longAdder.intValue());

        processor.onEvent(new AdminCommand("Puck", "createMischief", longAdder::increment));
        Assertions.assertEquals(3, longAdder.intValue());

        processor.onEvent(new AdminCommand("Aslam", "shutdown", longAdder::increment));
        Assertions.assertEquals(3, longAdder.intValue());

        commandAuthorizer.removeAuthorized(new CommandPermission("Puck", "createMischief"));
        processor.onEvent(new AdminCommand("Puck", "createMischief", longAdder::increment));
        Assertions.assertEquals(3, longAdder.intValue());
    }
}

4 - Build AOT processor

We are happy with the behaviour of our event processor and want this behaviour to be fixed at build time so our application is less dynamic and starts up more quickly.

  • Add the fluxtion maven plugin to our pom, configured to execute the scan task
  • Extend FluxtionGraphBuilder to build the processor and configure the generated source files
  • Run the build to generate the event processor AOT
  • Update the unit tests to reference the AOT processor, removing the Fluxtion.interpret calls

The interpreted version is refactored into the aot package

Aot builder

Refactor the processor building code into PermissionAotBuilder that will be called as part of the build process by the maven plugin. We may have to resolve source code generation issues at this point:

public class PermissionAotBuilder implements FluxtionGraphBuilder {
    @Override
    public void buildGraph(EventProcessorConfig eventProcessorConfig) {
        eventProcessorConfig.addNode(new CommandExecutor(new CommandAuthorizerNode()));
    }

    @Override
    public void configureGeneration(FluxtionCompilerConfig compilerConfig) {
        compilerConfig.setClassName("PermittedCommandProcessor");
        compilerConfig.setPackageName("com.fluxtion.example.devworkflow.aot.generated");
    }
}

AOT Unit test

The sample unit test is updated to use the AOT processor with this line changed

var processor = new PermittedCommandProcessor();

class CommandExecutorTest {
    @Test
    public void testPermission(){
        var processor = new PermittedCommandProcessor();
        processor.init();

        CommandAuthorizer commandAuthorizer = processor.getExportedService();
        commandAuthorizer.authorize(new CommandPermission("admin", "shutdown"));
        commandAuthorizer.authorize(new CommandPermission("admin", "listUser"));
        commandAuthorizer.authorize(new CommandPermission("Aslam", "listUser"));
        commandAuthorizer.authorize(new CommandPermission("Puck", "createMischief"));

        LongAdder longAdder = new LongAdder();
        processor.onEvent(new AdminCommand("admin", "shutdown", longAdder::increment));
        Assertions.assertEquals(1, longAdder.intValue());

        processor.onEvent(new AdminCommand("Aslam", "listUser", longAdder::increment));
        Assertions.assertEquals(2, longAdder.intValue());

        processor.onEvent(new AdminCommand("Puck", "createMischief", longAdder::increment));
        Assertions.assertEquals(3, longAdder.intValue());

        processor.onEvent(new AdminCommand("Aslam", "shutdown", longAdder::increment));
        Assertions.assertEquals(3, longAdder.intValue());

        commandAuthorizer.removeAuthorized(new CommandPermission("Puck", "createMischief"));
        processor.onEvent(new AdminCommand("Puck", "createMischief", longAdder::increment));
        Assertions.assertEquals(3, longAdder.intValue());
    }
}

Update pom for AOT generation

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.fluxtion.example</groupId>
        <artifactId>reference-examples</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <name>getting-started :: developer-workflow</name>
    <artifactId>developer-workflow</artifactId>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>com.fluxtion</groupId>
                <artifactId>fluxtion-maven-plugin</artifactId>
                <version>3.0.14</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>scan</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!--   UPDATE RUNTIME SCOPE, com.fluxtion:runtime NO LONGER SUPPLIED BY FLUXTION COMPILER-->
        <dependency>
            <groupId>com.fluxtion</groupId>
            <artifactId>runtime</artifactId>
            <version>9.3.8</version>
            <scope>compile</scope>
        </dependency>
        <!--  UPDATE PROVIDED SCOPE com.fluxtion:compiler NO LONGER REQUIREDAT RUNTIME-->
        <dependency>
            <groupId>com.fluxtion</groupId>
            <artifactId>compiler</artifactId>
            <version>9.3.8</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.1.0-jre</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

5 - Application integrating

Application integration is a simple matter of

  • creating an instance of PermittedCommandProcessor and call init
  • lookup exported service CommandAuthorizer
  • Either call service methods or onEvent
var processor = new PermittedCommandProcessor();
processor.init();
CommandAuthorizer commandAuthorizer = processor.getExportedService();