Event-Driven Hello World Program – DZone Microservices

Event-driven microservices can be straightforward to describe before implementing, testing, and maintaining. They are also highly responsive to new information in real time, with latencies in Java of below 10 microseconds 99.99% of the time, depending on the functionality of the small, independently deployable microservice.

In this introductory article, we use an example event-driven Hello World program (a programming paradigm where the program flow is determined by events) to step through behaviour-driven development, where we describe the behavior the business needs first as test data and writing a very simple microservice which turns input events like this:

say: Hello World 

Into outputs like this, by adding an exclamation point:

say: Hello World!  # <- adds an exclamation point 

All the code for this example is available on GitHub.

When modeling Event-Driven systems, a useful pattern is to have event-driven core systems with gateways connecting to external systems that might not be event-driven. To keep a clear separation of concern, business logic such as making a decision based on market data, or processing an order, is placed in the event-driven microservices, as these are the easiest to test, with the gateways connecting to external clients and systems being as thin as possible, so they are only concerned with acting as adapters and avoiding containing significant business logic.

Domain-Driven Design is a focus on determining the requirements of domain experts. Their requirements are further divided into event-driven microservices. Where the information is passed as a series of events between the microservices.

The requirements for each internal microservices can be described in YAML for Behavior-Driven Development.

All examples are in the Chronicle-Queue-Demo/hello-world module.

A Simple Event-Driven Contract

We model events as asynchronous method calls without arguments or one-to-many arguments eg

public interface Says {

   void say(String words);

}

This is the simplest Hello World example to get started. We can add to this interface other event types (methods) with multiple parameters. Parameters don’t have to be just primitives; they can also be complex data structures such as Data Transfer Objects.

There is no assumption about how the events produced by the microservice will be processed. It might be recorded but otherwise ignored, for now, processed immediately by a single microservice or read by multiple downstream microservices sometime later. Thus, it doesn’t return a value. Any results will be emitted as events from the respective event handlers. In programming, an event handler is a callback routine that can operate asynchronously.

External Event Producers and Consumers

Often we need to integrate with the client’s external systems. As this is a simple “Hello World” example, let’s imagine that instead of external systems connected via gateways, we have a simple program that reads input from the console to provide upstream events and another simple program to write to the console acting as a downstream gateway.

public class SaysInput {

   public static void input(Says says) throws IOException {

       BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

       for (String line; ((line = br.readLine()) != null); )

           says.say(line);

   }

}

public class SaysOutput implements Says {

   public void say(String words) {

       System.out.println(words);

   }

}

These can be integrated easily as the output of one is wired to the input of the other.

public class RecordInputToConsoleMain {

   public static void main(String[] args) throws IOException {

       // Writes text in each call to say(line) to the console

       final Says says = new SaysOutput();

       // Takes each line input and calls say(line) each time

       SaysInput.input(says);

   }

}

We can also record everything the producer performs to YAML to build tests later.

public class RecordInputAsYamlMain {

   public static void main(String[] args) throws IOException {

       // obtains a proxy that writes to the PrintStream the method calls and their  arguments

       final Says says = Wires.recordAsYaml(Says.class, System.out);

       // Takes each line input and calls say(theLine) each time

       SaysInput.input(says);

   }

}

Use the following to replay the output from a file.

public class ReplayOutputMain {

   public static void main(String[] args) throws IOException {

    // Reads the content of a Yaml file specified in args[0] and feeds it to SaysOutput.

     Wires.replay(args[0], new SaysOutput());

   }

}

Unit Tests for the RecordAsYaml and Replay Methods

To test the functionality of recordAsYaml and replay methods in isolation and verify if they work as suggested above, the following unit tests were developed. Having lots of text in unit tests is cumbersome, and in the next section, you can see how this text can be taken from files.

public class WiresTest extends WireTestCommon {



@Test

public void recordAsYaml() {

   ByteArrayOutputStream baos = new ByteArrayOutputStream();

   PrintStream ps = new PrintStream(baos);

   Says says = Wires.recordAsYaml(Says.class, ps);

   says.say("One");

   says.say("Two");

   says.say("Three");



   assertEquals("" +

           "---n" +

           "say: Onen" +

           "...n" +

           "---n" +

           "say: Twon" +

           "...n" +

           "---n" +

           "say: Threen" +

           "...n",

           new String(baos.toByteArray(), StandardCharsets.ISO_8859_1));

}



@Test

public void replay() throws IOException {

   ByteArrayOutputStream baos = new ByteArrayOutputStream();

   PrintStream ps = new PrintStream(baos);

   Says says = Wires.recordAsYaml(Says.class, ps);

   says.say("zero");

   Wires.replay("=" +

           "---n" +

           "say: Onen" +

           "...n" +

           "---n" +

           "say: Twon" +

           "...n" +

           "---n" +

           "say: Threen" +

           "...n",says);



   assertEquals("" +

           "---n" +

           "say: zeron" +

           "...n" +

           "---n" +

           "say: Onen" +

           "...n" +

           "---n" +

           "say: Twon" +

           "...n" +

           "---n" +

           "say: Threen" +

           "...n", new String(baos.toByteArray(), StandardCharsets.ISO_8859_1));

}



interface Says {

   void say(String word);

}

}

By recording and replaying using YAML, our microservices are written, tested, and debugged easily without any involvement of the messaging layer.

Let’s add a microservice as a data processor as a class that can have one or more event types. This microservice gets input events as text messages and adds an exclamation mark to them, and relays them to the output gateway.

public class AddsExclamation implements Says {

   private final Says out;



   public AddsExclamation(Says out) {

       this.out = out;

   }



   public void say(String words) {

       this.out.say(words + "!");

   }

}

A Single-Threaded Event-Driven Process

We can combine these all stages in one process, one thread. While this is putting unlikely to be useful in production, microservices into a single thread makes it easier to test and debug.

public class DirectWithExclamationMain {

   public static void main(String[] args) throws IOException {

       SaysInput.input(new AddsExclamation(new SaysOutput()));

   }

}

Testing a Single Event-Driven Service

Instead of embedding large amounts of text in a test, we can read resource files. This makes them easier to read and maintain.

public class AddsExclamationTest {

   @Test

   public void say() throws IOException {

YamlTester yt = YamlTester.runTest(AddsExclamation.class, "says");

assertEquals(yt.expected(), yt.actual());

   }

}

Let’s update the input to see how easy it is to maintain this test. I will change the second input to Hello World and run the test again.

src/test/resources/says/in.yaml
---

say: One

...

---

say: Hello World

...

---

say: Three

...

Not only does the test fail, but I can also on the differences to see clearly why.

At this point, I can either fix the test or I can accept the change by copying and pasting the actual result over the expected result in the out.yaml file.

In the next post, we will see how to implement a more realistic example of processing orders and automate many microservice tests from the configuration. This provides a basis for creating highly performant, deterministic, redundant microservices.

This article shows the outline of creating and testing a simple microservice, which provides the basis for microservices which are easy to deploy and maintain.

.

Leave a Comment