Simple Event Driven design

Overview

Developers often ask about the performance or efficiency of a system or their code. What does this really mean?
  • My code is so efficient, only a coding god could understand how it works.
  • My code is really clever but unmaintainable. The next developer will re-write it anyway.
  • My code is really efficient for the machine, but inefficient for the developer.
  • My code is really simple to understand which means the developer is more efficient, and the code is more than fast enough and easy to fix if not.
So instead of asking yourself how fast you can make the code and how many clever tricks you can put into it, ask yourself; how simple can I make this and still be more than fast enough?

Simple Event processing.

For me, the simplest event processing is a method call which doesn't return anything. This is simple to translate into an asynchronous messaging transport e.g.

public interface EventProcessor {
    void event(MyEventData data);
    
    void eventTwo(MyEventData2 data);
}

This is pretty simple, one component produces an event by calling a method, another component consumes or processes this event by providing an implementation.

How simple is it?

You can step from the producer component to the consumer component in your debugger using one button.

A couple of lines of code is needed to setup a unit test with a producer which calls your consumer.

MyConsumer mc = new MyEventProcessor();
MyProducer mp = new MyProducer(mc);

You can mock the Event Processor with any mock tool and check the producer creates the events you expect.  You can mock the producer by calling the methods on the consumer in the unit test.

How does it perform?

You might think this has next to no overhead, one component just calls another.  But even a method call has overhead, which is why the JIT supports inlining.  This can mean that the overhead is notional, and even less than that if the methods are optimised together (ie it can be faster than the sum of the two methods would take individually)

Am I missing something?

Actually there is an awful lot missing, but not relevant to the business requirements;
  • a transport.
  • monitoring
  • serialization
  • fail over
  • service discovery
  • security
These are separate concerns and usually not part of the essential complexity of the application.

What transports could I use?

There is so many to choose from, it's impossible to know what will be right for all cases in the future.  For this reason the choice of transport (or a lack of a transport) should be a configuration detail.  The only essential part of your design should be that the transport can be replaced easily without having to touch your business logic.

What an example of a transport?

A low latency, high throughput solution is to use Chronicle Queue. Repeating myself; you only need use this when it makes sense, or you could use any other transport.

What Chronicle Queue does;
  • persist every message for replay-ability and to check bug fixes.
  • low latency serialization with support for schema changes, and human readability for validation
  • logging and monitoring.
The last point is important. If you are already persisting every action your component takes and every state change, you shouldn't need any additional logging in normal operation. Any downstream component can re-create the state it is interested in without touching the component producing that information.

How does Chronicle Queue do this?

Chronicle Queue uses two components;
  • A writer which implements your interface. Each method call writes a message.
  • A reader which call an implementation of your interface. Each message calls the corresponding method.
Note: this strategy can be used for just about any transport.  What Chronicle Queue gives you is low latency recording or all messages for replay and replacement for logging.

How does this all perform?

If you use a flexible serialization like YAML, Binary YAML or JSON, and your component doesn't do much, you can expect to get a throughput of around 100,000 messages per second without much tuning.  If you use a lower level binary protocol, short messages and multiple threads, you can get over 10 Million messages per second.

You have the option to do all this GC free, but this can complicate your design so you are likely to create some garbage, but you have the option to reduce it as required.

Other notable transports.

Aeron is a low latency UDP based transport.

Chronicle Queue Enterprise supports replication and remote access over TCP.

Chronicle Websocket Jetty supports access to JSON via websocket for message rates around 100K/s

Conclusion

You should start your design and testing, focusing on the key components you actually need. You should allow your design to work for any transport, and have the option to replace one for the other.

In unit testing and debugging, it can be useful to have no transport to show that the components still work with a minimum of complexity.

How can we help?

Chronicle Software runs a one week on site workshop were we train/guide your team builds a prototype system using these design methodologies. The choice of solution is up to you so it can be a great way to start a new project and get hands on training which is immediately useful.  Contact sales@chronicle.software for more details.


Comments

Popular posts from this blog

Java is Very Fast, If You Don’t Create Many Objects

System wide unique nanosecond timestamps

Unusual Java: StackTrace Extends Throwable