Workflow Examples

Subscribe here to follow Infinitic's development. Please ⭐️ us on Github!

We will give here some examples of workflows to illustrate how powerful Infinitic is.

Bookings and Saga

We implement a booking process combining a car rental, a flight, and a hotel reservation. We require that all three bookings have to be successful together: if any of them fails, we should cancel the other successful bookings.

This is HotelBookingService's signature (CarRentalService and FlightBookingService's signatures are similar):

public interface HotelBookingService {
    HotelBookingResult book(HotelBookingCart cart);

    void cancel(HotelBookingCart cart);
}
interface HotelBookingService {
    fun book(cart: HotelBookingCart): HotelBookingResult

    fun cancel(cart: HotelBookingCart)
}

The orchestration of a complete booking will be done through the book method of BookingWorkflow:

public class BookingWorkflowImpl extends Workflow implements BookingWorkflow {
    // create stub for CarRentalService
    private final CarRentalService carRentalService = newTask(CarRentalService.class);
    // create stub for FlightBookingService
    private final FlightBookingService flightBookingService = newTask(FlightBookingService.class);
    // create stub for HotelBookingService
    private final HotelBookingService hotelBookingService = newTask(HotelBookingService.class);

    @Override
    public BookingResult book(
            CarRentalCart carRentalCart,
            FlightBookingCart flightCart,
            HotelBookingCart hotelCart
    ) {
        // dispatch parallel bookings using car, flight and hotel services
        Deferred<CarRentalResult> deferredCarRental =
                dispatch(carRentalService::book, carRentalCart);
        Deferred<FlightBookingResult> deferredFlightBooking =
                dispatch(flightBookingService::book, flightCart);
        Deferred<HotelBookingResult> deferredHotelBooking =
                dispatch(hotelBookingService::book, hotelCart);

        // wait and get result of deferred CarRentalService::book
        CarRentalResult carRentalResult = deferredCarRental.await();
        // wait and get result of deferred FlightService::book
        FlightBookingResult flightResult = deferredFlightBooking.await();
        // wait and get result of deferred HotelService::book
        HotelBookingResult hotelResult = deferredHotelBooking.await();

        // if all bookings are successful, we are done
        if (carRentalResult == CarRentalResult.SUCCESS &&
            flightResult == FlightBookingResult.SUCCESS &&
            hotelResult == HotelBookingResult.SUCCESS
        ) {
            return BookingResult.SUCCESS;
        }

        // else cancel all successful bookings in parallel
        if (carRentalResult == CarRentalResult.SUCCESS) { 
            dispatch(carRentalService::cancel, carRentalCart);
        }
        if (flightResult == FlightBookingResult.SUCCESS) { 
            dispatch(flightBookingService::cancel, flightCart);
        }
        if (hotelResult == HotelBookingResult.SUCCESS) { 
            dispatch(hotelBookingService::cancel, hotelCart);
        }

        return BookingResult.FAILURE;
    }
}
class BookingWorkflowImpl : Workflow(), BookingWorkflow {
    // create stub for CarRentalService
    private val carRentalService = newTask(CarRentalService::class.java)
    // create stub for FlightBookingService
    private val flightBookingService = newTask(FlightBookingService::class.java)
    // create stub for HotelBookingService
    private val hotelBookingService = newTask(HotelBookingService::class.java)

    override fun book(
        carRentalCart: CarRentalCart,
        flightCart: FlightBookingCart,
        hotelCart: HotelBookingCart
    ): BookingResult {
        // dispatch parallel bookings using car, flight and hotel services
        val deferredCarRental = dispatch(carRentalService::book, carRentalCart)
        val deferredFlightBooking = dispatch(flightBookingService::book, flightCart)
        val deferredHotelBooking = dispatch(hotelBookingService::book, hotelCart)

        // wait and get result of deferred CarRentalService::book
        val carRentalResult = deferredCarRental.await()
        // wait and get result of deferred FlightService::book
        val flightResult = deferredFlightBooking.await()
        // wait and get result of deferred HotelService::book
        val hotelResult = deferredHotelBooking.await()

        // if all bookings are successful, we are done
        if (carRentalResult == CarRentalResult.SUCCESS &&
            flightResult == FlightBookingResult.SUCCESS &&
            hotelResult == HotelBookingResult.SUCCESS
        ) {
            return BookingResult.SUCCESS
        }

        // else cancel all successful bookings in parallel
        if (carRentalResult == CarRentalResult.SUCCESS) { 
            dispatch(carRentalService::cancel, carRentalCart)
        }
        if (flightResult == FlightBookingResult.SUCCESS) { 
            dispatch(flightBookingService::cancel, flightCart)
        }
        if (hotelResult == HotelBookingResult.SUCCESS) { 
            dispatch(hotelBookingService::cancel, hotelCart)
        }

        return BookingResult.FAILURE
    }
}

This is really all we need to implement this workflow.

Inside a workflow, using the dispatch function triggers the execution of a task without blocking the flow of the workflow. Multiple uses of this function will trigger parallel executions of multiple tasks. The dispatch function returns a Deferred object, which is a reference to the dispatched task. By applying the await() method to it, we tell the workflow to wait for the task completion and to return its result.

Monthly invoicing

Let's consider now a workflow where, every month, we will:

  • use a Consumption service to get some metrics from a user
  • use a payment service to charge the user payment card
  • generate an invoice
  • send the invoice to the user

With Infinitic, we do not need any cron, writing such workflow is as simple as:

public class InvoicingWorkflowImpl extends Workflow implements InvoicingWorkflow {
    // create stub for ComsumptionService
    private final ComsumptionService comsumptionService = newTask(ComsumptionService.class);
    // create stub for PaymentService
    private final PaymentService paymentService = newTask(PaymentService.class);
    // create stub for InvoiceService
    private final InvoiceService invoiceService = newTask(InvoiceService.class);
    // create stub for EmainService
    private final EmainService emainService = newTask(EmainService.class);

    @Override
    public void start(User user) {
         // while this user is subscribed
         while (comsumptionService.isSubscribed(user)) {
            // get current date (inlined task)
            LocalDate now = inline(LocalDate::now);
            // get first day of next month
            LocalDate next = now.with(TemporalAdjusters.firstDayOfNextMonth());
            // wait until then
            timer(Duration.between(next, now)).await();
            // calculate how much the user will pay
            MonetaryAmount amount = comsumptionService.getMonetaryAmount(user, now, next);
            // get payment for the user
            paymentService.getPayment(user, amount);
            // generate the invoice
            Invoice invoice = invoiceService.create(user, amount, now, next);
            // send the invoice
            emailService.sendInvoice(user, invoice);
        }
    }
}
class InvoicingWorkflowImpl : Workflow(), InvoicingWorkflow {
    // create stub for ComsumptionService
    private final ComsumptionService comsumptionService = newTask(ComsumptionService::class.java)
    // create stub for PaymentService
    private final PaymentService paymentService = newTask(PaymentService::class.java)
    // create stub for InvoiceService
    private final InvoiceService invoiceService = newTask(InvoiceService::class.java)
    // create stub for EmainService
    private final EmainService emainService = newTask(EmainService::class.java)


    override fun start(user: User) {
        // while this user is subscribed
        while (comsumptionService.isSubscribed(user)) {
            // get current date (inlined task)
            val now = inline(LocalDate::now)
            // get first day of next month
            val next = now.with(TemporalAdjusters.firstDayOfNextMonth())
            // wait until then
            timer(Duration.between(next, now)).await()
            // calculate how much the user will pay
            val amount = comsumptionService.getMonetaryAmount(user, now, next)
            // get payment for the user
            paymentService.getPayment(user, amount)
            // generate the invoice
            val invoice = invoiceService.create(user, amount, now, next)
            // send the invoice
            emailService.sendInvoice(user, invoice)
        }
    }
}

Inside a workflow, awaiting a timer blocks the flow of the workflow up to the desired Instant or Duration (no resources are used during this waiting time).

Inside a workflow, all instructions must be deterministic - that's why the instruction LocalDate.now() must be in a task. Here, the inline function creates a pseudo-task inlined in the workflow.

A workflow must not contain a very high number of tasks, that's why loops should be avoided. Here, we have a limited number of possible iterations (running for 10 years will generate only 120 iterations) and 7 tasks per iteration. So we are fine in this case.

Loyalty program

Let's consider now a point-based loyalty program where:

  • users receive 10 points every week
  • users receive 100 points every time they complete a form
  • users receive 100 points every time they complete an order
  • users can burn points

With Infinitic, we can implement such a loyalty program like this:

public class LoyaltyWorkflowImpl extends Workflow implements LoyaltyWorkflow {
  
    // create stub for UserService
    private final UserService userService = newTask(UserService.class);
    
    // we store the number of points there
    private Int points = 0;

    @Override
    public void start(User user) {
        // while this user is subscribed
        while (userService.isActive(user)) {
            // wait one week
            timer(Duration.of(1, ChronoUnit.WEEKS)).await();
            // add points
            points += 10;
        }
    }

    @Override
    public void formCompleted() {
        points += 100;
    }

    @Override
    public void orderCompleted() {
        points += 500;
    }

    @Override
    public PointStatus burn(Int amount) {
        if (point - amount >= 0) {
            points -= amount;

            return PointStatus.OK;
        } else {
            return PointStatus.INSUFFICIENT;
        }
    }
}
class LoyaltyWorkflowImpl : Workflow(), LoyaltyWorkflow {
    
    // create stub for UserService
    val userService = newTask(UserService::class.java)
    
    // we store the number of points there
    var points = 0

    override fun start(user: User) {
        // while this user is subscribed
        while (userService.isActive(user)) {
            // wait one week
            timer(Duration.of(1, ChronoUnit.WEEKS)).await()
            // add points
            points += 10
        }
    }

    override formCompleted() {
        points += 100
    }

    override orderCompleted() {
        points += 500
    }

    override burn(Int amount) = 
        if (point - amount >= 0) {
            points -= amount

            PointStatus.OK
        } else {
            PointStatus.INSUFFICIENT
        }
}

An Infinitic client (or another workflow) can call methods of a running workflow. Multiple methods of the same workflow instance can run in parallel (but only one is running at a given time - one way to think of it is as an asynchronous but single-threaded execution)

Properties in workflows can be used to store information mutable from multiple methods.

A workflow must not contain a very high number of tasks, that's why loops should be avoided. Here we have a limited number of possible iterations (running for 10 years will generate 560 iterations only) and 2 tasks per iteration. So we are fine in this case.

Location Booking

Let's consider now an Airbnb-like service, where a traveler does a request to a host. The host will be notified of the request at most 3 times. If the response is positive, the traveler should pay a deposit, then both are notified.

This workflow could be implemented as such:

public class LoyaltyWorkflowImpl extends Workflow implements LoyaltyWorkflow {
    
    // create stub for HostService
    private final HostService hostService = newTask(HostService.class);

    // create stub for TravelerService
    private final TravelerService travelerService = newTask(TravelerService.class);

    // create stub for PaymentWorkflow
    private final PaymentWorkflow paymentWorkflow = newWorkflow(PaymentWorkflow.class);

    // create channel for BookingStatus
    final Channel<BookingStatus> responseChannel = channel();
    
    @Override
    public Channel<BookingStatus> getResponseChannel() {
        return responseChannel;
    }

    @Override
    public void start(Traveler traveler, Host host, LocationRequest request) {
        Object response;

        for (int i = 0; i < 3; i++) {
            // notify host of traveler request
            dispatch(hostService::notifyOfRequest, traveler, host, request);
            // start a timer for a day
            Deferred<Instant> timer = timer(Duration.of(1, ChronoUnit.DAYS));
            // start receiving signal in the channel
            Deferred<BookingStatus> signal = responseChannel.receive(1);
            // wait for the timer or the signal
            response = or(timer, signal).await();
            //  exit loop if we received a signal
            if (response instanceof BookingStatus) break;
        }

        // we did not receive host's response
        if (!(response instanceof BookingStatus)) {
            // notify host of traveler request
            dispatch(hostService::notifyExpiration, traveler, host, request);
            // notify host of traveler request
            dispatch(travelerService::notifyExpiration, traveler, host, request);
            // workflow stops here
            return;
        }

        // host did not accept the request
        if (response == BookingStatus.DENIED) {
            // notify host of traveler request
            dispatch(travelerService::notifyDenial, traveler, host, request);
            // workflow stops here
            return;
        }

        // trigger deposit workflow and wait for it
        paymentWorkflow.getDeposit(traveler, host, request);

        // notify host of the succesful booking
        dispatch(hostService::notifyBooking, traveler, host, request);

        // notify traveler of the succesful booking
        dispatch(travelerService::notifyBooking, traveler, host, request);
    }
}
public class LoyaltyWorkflowImpl: Workflow(), LoyaltyWorkflow {
    
    // create stub for HostService
    val hostService = newTask(HostService.class)

    // create stub for TravelerService
    val travelerService = newTask(TravelerService.class)

    // create stub for PaymentWorkflow
    val paymentWorkflow = newWorkflow(PaymentWorkflow.class)

    // create channel for BookingStatus
    val responseChannel = channel<BookingStatus>()

    override fun start(traveler: Traveler, host: Host, request: LocationRequest) {
        var response: Any

        for (int i = 0; i < 3; i++) {
            // notify host of traveler request
            dispatch(hostService::notifyOfRequest, traveler, host, request)
            // start a timer for a day
            val timer = timer(Duration.of(1, ChronoUnit.DAYS))
            // start receiving signal in the channel
            val signal = responseChannel.receive(1)
            // wait for the timer or the signal
            response = (timer or signal).await();
            //  exit loop if we received a signal
            if (response instanceof BookingStatus) break;
        }

        // we did not receive host's response
        if (response !instanceof BookingStatus) {
            // notify host of traveler request
            dispatch(hostService::notifyExpiration, traveler, host, request)
            // notify host of traveler request
            dispatch(travelerService::notifyExpiration, traveler, host, request)
            // workflow stops here
            return
        }

        // host did not accept the request
        if (response  == BookingStatus.DENIED) {
            // notify host of traveler request
            dispatch(travelerService::notifyDenial, traveler, host, request)
            // workflow stops here
            return
        }

        // trigger deposit workflow and wait for it
        paymentWorkflow.getDeposit(traveler, host, request)

        // notify host of the succesful booking
        dispatch(hostService::notifyBooking, traveler, host, request)

        // notify traveler of the succesful booking
        dispatch(travelerService::notifyBooking, traveler, host, request)
    }
}

We can send external signals to a workflow to notify it that something happened. A signal is a serializable object. To receive a signal, a workflow must have a channel.

As illustrated here with the PaymentWorkflow, a workflow can dispatch (synchronously or asynchronously) another sub-workflow. It opens unlimited possibilities.

Edit this page on GitHub Updated at Sat, Sep 10, 2022