Event-Driven Order Processing Program
Building, testing, and fixing.
Join the DZone community and get the full member experience.
Join For FreeFollowing the Hello World example of a simple, independently deployable real-time Event-Driven microservice, this article looks at a more realistic example of an Order Processor with a New Order Single in and an Execution Report out.
A New Order Single is a standard message type for the order of one asset in the FIX protocol used widely by financial institutions such as banks. The reply is typically one or more Execution Reports updating the status of that order.
Some Background on Fintech
In fintech, when one organisation wishes to purchase an asset or commodity from another, they send an order.
The other organisation sends back a message to notify if the order was successful; this message is called an execution report. You could think of it a bit like a trade receipt. These orders and execution reports are transmitted electronically, using a data format standardised by Financial Information eXchange (FIX). There are many different types of orders, but one of the most popular Orders offered by the FIX standard is the NewOrderSingle.
We use the same terminology as the FIX protocol to simplify the translation from one to the other. This example is also available on GitHub.
Again, we model an input event in YAML. To start with, we will reject all new orders as this is simple to demonstrate.
Testing This Service
We can test this service with the captured data earlier with a YAML configuration. We override the system clock to produce the same results every time.
public static void runTest(String path) {
try {
SystemTimeProvider.CLOCK = new SetTimeProvider("2019-12-03T09:54:37.345678")
.advanceMicros(1);
YamlTester yt = YamlTester.runTest(OMSImpl.class, path);
assertEquals(yt.expected(), yt.actual());
} finally {
SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE;
}
}
@Test
public void newOrderSingle() {
runTest("newOrderSingle");
}
As in previous examples, if the output is incorrect, we can quickly see this in the data.
What Do We See When a Test Fails?
Say we don’t override the time and use the wall clock instead. We might see something like this.
And if we “Click to see the difference”, we can see the orderID, which contains a timestamp that has changed. There are many ways to handle this, but we override the system clock to ensure we also get the same time in this example.
The component doesn’t need to log anything as all results, including errors, are written to the output queue.
Performance Testing
In this benchmark, 100k orders/s are injected into one queue. These are then processed, and the result is written in a second queue and read to get an end-to-end latency. Each run is 30 seconds. This times two serializations, two writes, two reads, two deserializations, and two hops between threads.
Running on a desktop with an AMD Ryzen 9 5950X and Ubuntu 21.10, gives very stable performance for in-memory messaging, and consistent latencies for writing to the memory-mapped file synchronously. Using the queues in an asynchronous mode achieves similar latencies to in-memory writes, while persisting to disk as fast as possible.
Comparing using tmpfs as in-memory and ext4 on an M.2 NVMe drive.
Conclusion
Combining high performance and ease of use can be challenging. However, if you keep it simple, microservices using event sources as input and outputs, can have consistent, microsecond latency and support maintainable tests.
Opinions expressed by DZone contributors are their own.
Comments