1st tutorial - Spring


Table of contents
  1. Introduction
  2. Example project
  3. The Lottery game
  4. Designing the components
    1. Spring config
    2. Service api
    3. Implementing logic
      1. TicketStoreNode
      2. LotteryGameNode
  5. Building the application
    1. Exporting services
    2. Accessing exported services
    3. Event dispatch
    4. Lifecycle methods
    5. Wiring the components together
    6. Build system
  6. Running the application
  7. Conclusion

Introduction


This tutorial is an introduction to writing event driven application logic using Fluxtion and Spring. The reader should be proficient in Java, maven, git and possess a basic knowledge of Spring dependency injection. Spring is a very popular dependency injection container, this example demonstrates Fluxtion Spring integration.

Our goal is to build the logic for a simple lottery application that will be connected to request and response queues.

  • Logic resides in user classes and functions
  • Spring config declares which beans to wire together
  • Fluxtion creates an event processor that manages the beans and dispatches events to the correct bean instance

This example is focused on building event driven processing logic and not the connection to real queues.

At the end of this tutorial you should understand how Fluxtion:

  • Exposes service interfaces for managed components
  • Calls lifecycle methods on managed components
  • Triggers event logic between dependent components
  • Wires components together using Spring configuration

Example project

The example project is referenced in this tutorial.

The Lottery game

A lottery game sells tickets to customers from a ticket shop, the shop is either opened or closed. A customer receives a receipt for a purchased ticket or a message that no ticket was purchased. Tickets must have six numbers and cannot be bought when the shop is closed. A lottery machine picks the winning ticket number from the tickets purchased and publishes the lucky number to a queue.

Designing the components

Our application will be event driven through a service interface api for the outside world to code against. We must first think about the design of our services and then the concrete implementations. Once this design is complete we will use Fluxtion to wire up the components. Fluxtion is low touch allowing engineers and architects to concentrate on design and components with no distraction.

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

    buyTicket><b>ServiceCalls</b>\n buyTicket, openStore, closeStore, setTicketSalesPublisher]:::eventHandler
    selectWinningTicket><b>ServiceCalls</b>\n selectWinningTicket, setResultPublisher]:::eventHandler


    LotteryMachine([<b>ServiceLookup</b>::LotteryMachine]):::exportedService
    TicketStore([<b>ServiceLookup</b>::TicketStore]):::exportedService
    
    TicketStoreNode[TicketStoreNode\n <b>ExportService</b>::TicketStore]:::graphNode
    LotteryMachineNode[LotteryMachineNode\n <b>ExportService</b>::LotteryMachine]:::graphNode

    selectWinningTicket ---> LotteryMachine
    buyTicket --> TicketStore
    
    LotteryMachine --> LotteryMachineNode
    TicketStore ---> TicketStoreNode
 
    subgraph EventProcessor
        TicketStoreNode --> LotteryMachineNode
    end

Spring config

Spring config for our lottery application

<beans xmlns="">
    <bean id="ticketStore" class="com.fluxtion.example.cookbook.lottery.nodes.TicketStoreNode"/>
    <bean id="lotteryMachine" class="com.fluxtion.example.cookbook.lottery.nodes.LotteryMachineNode">
        <constructor-arg ref="ticketStore"/>
    </bean>
</beans>

Service api

From our business problem we have identified a concrete data type Ticket and two public services TicketStore and LotteryMachine. Now we have identified the top level concepts we can create a service api that client code will use to drive the system.

public record Ticket(int number, UUID id) {
    public Ticket(int number){
        this(number, UUID.randomUUID());
    }
}

public interface TicketStore {
    boolean buyTicket(Ticket ticket);
    void openStore();
    void closeStore();
    void setTicketSalesPublisher(Consumer<String> ticketSalesPublisher);
}

public interface LotteryMachine {
    void selectWinningTicket();
    void setResultPublisher(Consumer<String> resultPublisher);
}

Our interfaces separate concerns logically making the api simple to work with. The methods setTicketSalesPublisher and setResultPublisher connect the results of processing to output queues or a unit test. One of our goals is to make the logic easy to test with the minimum of infrastructure.

Implementing logic

We implement our two interfaces with concrete classes TicketStoreNode and LotteryGameNode using some lombok annotations to remove boilerplate code.

TicketStoreNode

The TicketStoreNode implements TicketSore and supports logic to buy and sell tickets depending on the state of the store . A lifecycle method start is created that checks the ticketSalesPublisher has been set before progressing any further. TicketStoreNode also implements Supplier<Ticket> which allows any child component to access the last sold ticket without accessing the concrete type. Making components reference each other through interfaces is good practice.

@Slf4j
public class TicketStoreNode implements Supplier<Ticket>, TicketStore {

    private boolean storeOpen;
    private Consumer<String> ticketSalesPublisher;
    private Ticket ticket;

    @Override
    public void setTicketSalesPublisher(Consumer<String> ticketSalesPublisher) {
        this.ticketSalesPublisher = ticketSalesPublisher;
    }

    public void start() {
        Objects.requireNonNull(ticketSalesPublisher, "must have a ticketSalesPublisher set");
        storeOpen = false;
    }

    @Override
    public boolean buyTicket(Ticket ticket) {
        if (ticket.number() < 9_99_99 | ticket.number() > 99_99_99) {
            ticketSalesPublisher.accept("invalid numbers " + ticket);
            this.ticket = null;
        } else if (storeOpen) {
            ticketSalesPublisher.accept("good luck with " + ticket);
            this.ticket = ticket;
        } else {
            ticketSalesPublisher.accept("store shut - no tickets can be bought");
            this.ticket = null;
        }
        return this.ticket != null;
    }

    @Override
    public Ticket get() {
        return ticket;
    }

    @Override
    public void openStore() {
        log.info("store opened");
        storeOpen = true;
    }

    @Override
    public void closeStore() {
        log.info("store closed");
        storeOpen = false;
    }
}

LotteryGameNode

The LotteryMachineNode implements LotteryMachine and supports logic to run the lottery. LotteryMachineNode holds a reference to an instance of Supplier<Ticket> and whenever processNewTicketSale is called, acquires a purchased ticket and adds it to the internal cache. A lifecycle method start is created that checks the resultPublisher has been set before progressing any further.

@Slf4j
@RequiredArgsConstructor
public class LotteryMachineNode implements LotteryMachine {

    private final Supplier<Ticket> ticketSupplier;
    private final transient List<Ticket> ticketsBought = new ArrayList<>();
    private Consumer<String> resultPublisher;

    @Override
    public void setResultPublisher(Consumer<String> resultPublisher) {
        this.resultPublisher = resultPublisher;
    }

    public void start(){
        Objects.requireNonNull(resultPublisher, "must set a results publisher before starting the lottery game");
        log.info("started");
    }

    public boolean processNewTicketSale() {
        ticketsBought.add(ticketSupplier.get());
        log.info("tickets sold:{}", ticketsBought.size());
        return false;
    }

    @Override
    public void selectWinningTicket() {
        if(ticketsBought.isEmpty()){
            log.info("no tickets bought - no winning ticket");
        }else {
            Collections.shuffle(ticketsBought);
            log.info("WINNING ticket {}", ticketsBought.get(0));
        }
        ticketsBought.clear();
    }
}

The lifecycle methods and how clients access the TicketStore and LotteryMachine services are described below.

Building the application

Now we have our service interfaces designed and implemented we need to connect components together and make sure they provide the functionality required in the expected manner. There are several problems to solve to deliver correct event driven functionality:

  • How do clients access the components via service interfaces
  • How are the lifecycle methods called
  • How is LotteryGameNode#processNewTicketSale called only when a ticket is successfully purchased
  • How are the components wired together

Fluxtion solves these four problems for any event driven application.

Exporting services

We want clients to access components via service interface, this is simple to achieve by adding an @ExportService annotation to the interface definitions on the concrete classes, as shown below.

import com.fluxtion.runtime.annotations.ExportService;
public class LotteryMachineNode implements @ExportService LotteryMachine {
  //removed for clarity
}

import com.fluxtion.runtime.annotations.ExportService;
public class TicketStoreNode implements Supplier<Ticket>, @ExportService TicketStore {
  //removed for clarity
}

Fluxtion will only export annotated interfaces at the container level, in this case Fluxtion will not export the Supplier<Ticket> interface that TicketStoreNode implements.

Accessing exported services

Once the service interface has been marked for export client code can locate it through the EventProcessor instance that holds the application components by calling EventProcessor#getExportedService. Client code invokes methods on the interface and Fluxtion container will take care of all method routing.

public static void start(Consumer<String> ticketReceiptHandler, Consumer<String> resultsPublisher){
    EventProcessor lotteryEventProcessor = //removed for clarity
    LotteryMachine lotteryMachine = lotteryEventProcessor.getExportedService();
    TicketStore ticketStore = lotteryEventProcessor.getExportedService(); 
    lotteryMachine.setResultPublisher(resultsPublisher);
    ticketStore.setTicketSalesPublisher(ticketReceiptHandler);
}

Event dispatch

When a ticket has been successfully purchased the LotteryMachineNode instance method processNewTicketSale is invoked by Fluxtion. The processNewTicketSale method grabs the last ticket sale from the Supplier<Ticket> reference and adds it to the cache. Fluxtion knows to trigger a method if it is annotated with @OnTrigger and one of its dependencies has been triggered from an incoming client service call.

public class LotteryMachineNode implements LotteryMachine {
  //code removed for clarity

  @OnTrigger
  public boolean processNewTicketSale() {
    ticketsBought.add(ticketSupplier.get());
    log.info("tickets sold:{}", ticketsBought.size());
    return false;
  }
}

How does Fluxtion know to invoke this method at the correct time? The container maps the dependency relationship between TicketStoreNode and LotteryMachineNode, so when an exported service method is invoked on TicketStoreNode Fluxtion calls the processNewTicketSale trigger method on LotteryMachineNode. This is great as it removes the need for the programmer to manually call the event dispatch call graph.

The next problem is we only want the processNewTicketSale method called when a ticket is successfully purchased. If we try to add a ticket when the openStore is called a null pointer exception will be thrown at runtime. How can the developer control the propagation of dependent trigger methods?

Fluxtion manages exported service event propagation in two ways:

  • boolean return type from the service method, false indicates no event propagation, true propagates the notification
  • annotate the method with @NoPropagateFunction annotation

Both propagation controls are used in LotteryMachineNode ensuring the LotteryMachine is only triggered on successful ticket purchases. The TicketStoreNode#buyTicket is the only method that will trigger an event notification to LotteryMachineNode and only if the ticket passes basic validation and the store is open.

public class TicketStoreNode implements Supplier<Ticket>, @ExportService TicketStore {
    //code removed for clarity
    
    @Override
    @NoPropagateFunction
    public void setTicketSalesPublisher(Consumer<String> ticketSalesPublisher) {}
    
    public void start() {}
    
    //return true -> triggers event propagation
    public boolean buyTicket(Ticket ticket) {
      if (ticket.number() < 9_99_99 | ticket.number() > 99_99_99) {
          ticketSalesPublisher.accept("invalid numbers " + ticket);
          this.ticket = null;
      } else if (storeOpen) {
          ticketSalesPublisher.accept("good luck with " + ticket);
          this.ticket = ticket;
      } else {
          ticketSalesPublisher.accept("store shut - no tickets can be bought");
          this.ticket = null;
      }
      return this.ticket != null;
    }
    
    public Ticket get() {}
    
    @NoPropagateFunction
    public void openStore() {}
    
    @NoPropagateFunction
    public void closeStore() {}
}

Lifecycle methods

Applications often benefit from lifecycle methods such as init, start and stop, allowing checks to be carried out before executing the application. Fluxtion supports init, start and stop by annotating a method with an annotation @Start @Stop or @Initialise. We use the start method in our application to check output receivers ticketSalesPublisher and resultPublisher have been set by the client code.

public class TicketStoreNode implements Supplier<Ticket>, @ExportService TicketStore {
  //code removed for clarity
    @Start
    public void start() {
        Objects.requireNonNull(ticketSalesPublisher, "must have a ticketSalesPublisher set");
        storeOpen = false;
    }
}

public class LotteryMachineNode implements @ExportService LotteryMachine {
  //code removed for clarity
    @Start
    public void start(){
        Objects.requireNonNull(resultPublisher, "must set a results publisher before starting the lottery game");
        log.info("started");
    }
}

Client code invokes the lifecycle method on the container Fluxtion then calls all the lifecycle methods registered by components in the right order.

public static void start(Consumer<String> ticketReceiptHandler, Consumer<String> resultsPublisher){
    EventProcessor lotteryEventProcessor = //removed for clarity
    lotteryEventProcessor.init();
    LotteryMachine lotteryMachine = lotteryEventProcessor.getExportedService();
    TicketStore ticketStore = lotteryEventProcessor.getExportedService();
    lotteryMachine.setResultPublisher(resultsPublisher);
    ticketStore.setTicketSalesPublisher(ticketReceiptHandler);
    lotteryEventProcessor.start();
}

Wiring the components together

The dependency injection container wires components depending upon the configuration supplied. As Fluxtion natively supports
spring ApplicationContext we use a spring configuration file in this example to wire the TicketStore to the LotteryMachine. We are using Spring in these tutorials because of its familiarity to readers, spring is not required by Fluxtion when using other methods to specify container managed beans.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="">
    <bean id="ticketStore" class="com.fluxtion.example.cookbook.lottery.nodes.TicketStoreNode"/>

    <bean id="lotteryMachine" class="com.fluxtion.example.cookbook.lottery.nodes.LotteryMachineNode">
        <constructor-arg ref="ticketStore"/>
    </bean>
</beans>

Fluxtion provides a spring extension for building a container using static helper methods. The built container is free of any spring dependencies, Fluxtion just reads the spring file to drive its own configuration. To build the container the tutorial loads the spring file from the classpath:

public static void start(Consumer<String> ticketReceiptHandler, Consumer<String> resultsPublisher){
  EventProcessor lotteryEventProcessor = FluxtionSpring.interpret(
    new ClassPathXmlApplicationContext("com/fluxtion/example/cookbook/lottery/spring-lottery.xml"));
  //removed for clarity
}

Build system

The example use maven to build the application, the Fluxtion runtime dependency is pulled in transitively via the compiler. Lombok is added to reduce boilerplate code, spring-context enables reading the spring config file, both of these dependencies are optional in vanilla Fluxtion usage.

<?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>
<groupId>com.fluxtion.example</groupId>
<artifactId>getting-started-tutorial1</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>getting-started :: tutorial 1 :: lottery</name>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <fluxtion.version>9.3.47</fluxtion.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.fluxtion</groupId>
            <artifactId>compiler</artifactId>
            <version>${fluxtion.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.29</version>
        </dependency>
    </dependencies>
</project>

Running the application

Running the application requires the following from client code:

  • Building the container using the spring config file
  • Call lifecycle methods on the container
  • Lookup container exported service interfaces and store the references for use in client code

The fact the components are managed by a container is completely hidden from the client code, this makes integrating Fluxtion into an existing system extremely simple as no new programming models need to be adopted.

In our example the main method only interacts with the business logic via the service interfaces, in a real application the methods would be invoked by taking commands from an incoming request queue.

public class LotteryApp {

    private static LotteryMachine lotteryMachine;
    private static TicketStore ticketStore;

    public static void main(String[] args) {
        start(LotteryApp::ticketReceipt, LotteryApp::lotteryResult);
        //try and buy a ticket - store is closed
        ticketStore.buyTicket(new Ticket(12_65_56));

        //open store and buy ticket
        ticketStore.openStore();
        ticketStore.buyTicket(new Ticket(12_65_56));
        ticketStore.buyTicket(new Ticket(36_58_58));
        ticketStore.buyTicket(new Ticket(73_00_12));

        //bad numbers
        ticketStore.buyTicket(new Ticket(25));

        //close the store and run the lottery
        ticketStore.closeStore();

        //try and buy a ticket - store is closed
        ticketStore.buyTicket(new Ticket(12_65_56));

        //run the lottery
        lotteryMachine.selectWinningTicket();
    }

    public static void start(Consumer<String> ticketReceiptHandler, Consumer<String> resultsPublisher){
        var lotteryEventProcessor = FluxtionSpring.interpret(
                new ClassPathXmlApplicationContext("com/fluxtion/example/cookbook/lottery/spring-lottery.xml"));
        lotteryEventProcessor.init();
        lotteryMachine = lotteryEventProcessor.getExportedService();
        ticketStore = lotteryEventProcessor.getExportedService();
        lotteryMachine.setResultPublisher(resultsPublisher);
        ticketStore.setTicketSalesPublisher(ticketReceiptHandler);
        lotteryEventProcessor.start();
    }

    public static void ticketReceipt(String receipt){
        log.info(receipt);
    }

    public static void lotteryResult(String receipt){
        log.info(receipt);
    }
}

Executing our application produces the following output:

[main] INFO LotteryMachineNode - started
[main] INFO LotteryApp - store shut - no tickets can be bought
[main] INFO TicketStoreNode - store opened
[main] INFO LotteryApp - good luck with Ticket[number=126556, id=77376783-3513-4f22-88be-5ace6cdf5839]
[main] INFO LotteryMachineNode - tickets sold:1
[main] INFO LotteryApp - good luck with Ticket[number=365858, id=05e6f44e-5938-4b28-a183-047c6e75c532]
[main] INFO LotteryMachineNode - tickets sold:2
[main] INFO LotteryApp - good luck with Ticket[number=730012, id=30af94d7-7aec-4e82-8159-2cade3b38b2b]
[main] INFO LotteryMachineNode - tickets sold:3
[main] INFO LotteryApp - invalid numbers Ticket[number=25, id=62afdb45-25f8-4a80-bfba-37c22bfe8bf2]
[main] INFO TicketStoreNode - store closed
[main] INFO LotteryApp - store shut - no tickets can be bought
[main] INFO LotteryMachineNode - WINNING ticket Ticket[number=365858, id=05e6f44e-5938-4b28-a183-047c6e75c532]

Process finished with exit code 0

Conclusion

We have quite a lot of ground in a seemingly simple event driven application. Hopefully you can see the benefits of using Fluxtion to write event driven business logic:

  • Forcing client code to interact with components through interfaces
  • Formal lifecycle phases that are easy to plug in to
  • Dispatch of events to any bean managed instance exporting a service
  • Automatic dispatch of dependent trigger methods
  • Removal of state and conditional dispatch logic from business code
  • Deterministic event handling that is well tested
  • Control of event propagation with simple boolean returns or annotations
  • More time spent writing logic and less time writing infrastructure
  • Simple programming model that leverages Spring for quick adoption

As applications grow and become more complex programming event driven logic becomes ever more expensive. The benefits of a tool like Fluxtion really shine during the growth and maintenance phase.

I hope you have enjoyed reading this tutorial, and it has given you an understanding of Fluxtion and a desire to use it in your applications. Please send me in any comments or suggestions to improve this tutorial

next tutorial 2