Enterprise Integration Patterns
Gregor's Ramblings
HOME PATTERNS RAMBLINGS ARTICLES TALKS DOWNLOAD BOOKS CONTACT

TechEd 2005 Europe: Event-Driven Architectures

July 11, 2005

Gregor HohpeHi, I am Gregor Hohpe, co-author of the book Enterprise Integration Patterns. I like to work on and write about asynchronous messaging systems, service-oriented architectures, and all sorts of enterprise computing and architecture topics. I am also an Enterprise Strategist at AWS.
TOPICS
ALL RAMBLINGS  Architecture (12)  Cloud (10)  Conversations (8)  Design (26)  Events (27)  Gregor (4)  Integration (19)  Messaging (12)  Modeling (5)  Patterns (8)  Visualization (3)  WebServices (5)  Writing (12) 
POPULAR RAMBLINGS
RECENT

My blog posts related to IT strategy, enterprise architecture, digital transformation, and cloud have moved to a new home: ArchitectElevator.com.

My second talk at TechEd Europe was a rapid journey through call-stack semantics, coupling, composability, and visualizations, with a bit of test-driven development, inversion of control, and Resharper mixed in. Since I had a large audience and got decent feedback ratings (aside from the comment that I tend to look like I am running for a train...) I figured it might be worth sharing some of the talk as a blog post. The talk is obviously inspired by some of my earlier postings, namely Good Composers are Far and Few in Between, Look Ma -- No Middleware! , and Visualizing Dependencies so I won't go into a ton of detail on those topics but invite you to (re)read these earlier posts. Also, since this talk was part of a Microsoft conference all code examples are in C#. However, I won't dwell on C# language features. Everything I show here would look very similar in Java (except for the stupid "I" in front of the interface names).

The Call Stack

The call stack is one of these things that we rarely think about (except when we get a stack trace) but that actually governs the way we think and write software in a fairly significant way. Most high-level programming languages like Java, C#, C++, VB are based on a stack model. Even declarative languages like XSLT support a call-stack model through elements like call-template (even though this can lead to more pain than gain). The call stack actually provides a few critical functions for us:

OK, so you might feel like you are having back flashs from CompSci 101. Why is this interesting to professional developers? It becomes interesting when you use a programming model that does not have a built-in call stack. Particularly, because all the assumptions that you became used to making are no longer given. This can give you quite a headache.

For example, a messaging model does not support a call stack. When I send a request message and receive a response message at a later time, none of the mechanisms mentioned above are at work. There is no coordination unless I put in an active wait loop. The message does actually not come back to me unless I specifically supply a Return Address. Lastly, I do not have my local state when I receive the response message unless I use a Correlation Identifier or include all necessary state in the message.

Some messaging approaches are trying to put the call stack back into the model but this is generally a bad idea.

Composability

Another key ingredient into event-driven architectures is the notion of composability. Composability is the ability to build new things from existing pieces. This can occur because the individual pieces make fewer assumptions about other components and therefore do not presuppose the overall structure of the system. This is one of the key benefits of loose coupling: the fewer assumptions I make about the components I interact with the easier it is to change the other components. Composability is a great benefit because it promotes reuse and adaptability. It is also great during testing -- I might want to swap out another component for a test or mock component during testing. If my system is composable, that is very easy to do.

A key aspect of composability is that something must perform the composition. We can distinguish there between explicit composition and implicit composition. Explicit composition is one where a special Assembler wires two (or more) components together. Implicit composition takes place when one component decides by itself (or is configure to) publish data to Channel X and another component is setup to receive data from Channel X. Because both components independently decided to work with the same channel they will exchange data without knowing the other party. Because there is no Assembler needed, we refer to this approach as implicit composition.

Event-Driven Architectures

Event-driven architectures are those that expose a top-layer structure that is not dependent on a call stack and is highly composable. The high-level components communicate solely through events that are passed through event channels. For example, one component might receive orders via a Web site or a Web service. Once the component did some basic validation it publishes a message "Order Received". It is completely oblivious to which other components listen and react to this event. It also does not presuppose what actions should happen next. This is very different from a method call where I specifically instruct another component to perform a function of my choice.

I mentioned "top-layer structure" because each individual component is most likely constructed using regular, call-stack oriented techniques. This is just fine because the inside of a component benefits from the much richer interaction model that a method call gives us. It would be a huge pain to have to correlate and synchronize for every method call! Also, the tighter coupling inside a component is a non-issue because typically one person or one team develops and controls all pieces of the component.

A Simple Event Channel

Alright, let's go ahead and build some of the core pieces to build an EDA. The first important piece we need are the event channels, i.e. the pieces that allow us to publish events and subscribe to them. We'll build those from scratch here. In order to focus on architectural trade-offs instead of language features I intentionally keep things very simple. This is partly just practicality (call it laziness if you wish) but that aside it is still a good idea to keep your channels simple. The channel behavior and the events are the key coupling point between components. The more complex these things are the more implicit coupling occurs between the components.

What does a very simple channel have to do? It has to allow on component to send message and another one to receive them. Going back to basics here, the interface can look like this:

public delegate void OnMsgEvent(XmlDocument msg);

public interface IMessageReceiver
{
    void Subscribe(OnMsgEvent handler); 
}
    
public interface IMessageSender
{
    void Send(XmlDocument msg);
}

public interface IChannel : IMessageSender, IMessageReceiver
{}

It is common wisdom that a good design is not the one where nothing can be added but rather one where nothing can be taking away. It seems like we are pretty close to that design goal here :-) We have a method to send an event in form of an XML document and a method to signify our interest in receiving all events coming in on this channel. This is done through delegates, making for a Event-driven Consumer (note that an Event-driven consumer itself has little to do with Event-driven architectures -- one is a microscoping implementation decision inside an endpoint and the other is an architectural style).

So far so good, all we need to do now is implement this interface. That should not be too hard given that all we have is two methods. But not so fast, there are a few more things we need to decide on...

Design Decisions

The IChannel interface merely specifies the syntax of our channel interface. The name and parameters of our methods suggest some semantics, i.e. a it is fairly likely that a method named send that takes an XmlDocument does in fact send a message. But this assumption leaves quite a bit to be desired in terms of precision. Let's look at a few more details that I would like to know about a channel:

In some of these cases you might be inclined to argue that a component should not have to know how the channel behaves in detail. This is true as long as the component does not make any implicit assumptions that might be fulfilled by some channels and not others. For example, if a component that receives a message modifies that message it makes an implicit assumption that it received its own copy of the message. This will work for distibuted systems but may not work for a simple local channel that simply sends a copy of the same message.

Specifying Behavior

Because the interface itself does not say much about the expected behavior of the channel we need to find a better way to express what we decided. The best way to do that is code -- test code. So let's whoop up some nUnit test cases to specify the behavior we discussed. First, we want to make sure that basic composition works. We also want to verify that our channel is a Publish-subscribe Channel and that it sends copies of the inbound messages. This makes for three test cases:

The code for the first test looks like this:

[Test]
public void SequentialComposition()
{
	DebugChannel inChannel = new DebugChannel(new StraightThroughChannel());
	DebugChannel connChannel = new DebugChannel(new StraightThroughChannel());
	DebugChannel outChannel = new DebugChannel(new StraightThroughChannel());

	new TraceFilter(inChannel, connChannel, "filter1");
	new TraceFilter(connChannel, outChannel, "filter2");

	inChannel.Send(doc);

	Assert.AreEqual("filter1", outChannel.LastMessage.DocumentElement.ChildNodes[0].Name);
	Assert.AreEqual("filter2", outChannel.LastMessage.DocumentElement.ChildNodes[1].Name);
}

This code uses two helper classes. One is the DebugChannel. This channel is a Decorator around our simple channel that allows us to inspect the last message that was sent. The other one is the TraceFilter, a filter that appends its name to any message that passes through. The filter takes an input and an output channel in its constructor along with the name of the channel. The test explicitly composes two filters to that the message passes through them sequentially (see figure). At the end it verifies that the message passed through filter1 first and through filter2 last. By the way, the doc variable has been initialized by the [SetUp] method that nUnit calls before every test case.

In order to verify that we have a Pub-Sub Channel we need to hook two subscribers to the same channel. We do this again with two TraceFilters. This time we verify that each output event has passed through exactly on filter and has been tagged by that filter:

[Test]
public void ChannelSendsMessageToAllSubscribers()
{
    DebugChannel inChannel = new DebugChannel(new StraightThroughChannel());
    DebugChannel out1Channel = new DebugChannel(new StraightThroughChannel());
    DebugChannel out2Channel = new DebugChannel(new StraightThroughChannel());

    new TraceFilter(inChannel, out1Channel, "filter1");
    new TraceFilter(inChannel, out2Channel, "filter2");

    inChannel.Send(doc);

    Assert.AreEqual(1, out1Channel.LastMessage.DocumentElement.ChildNodes.Count);
    Assert.AreEqual("filter1", out1Channel.LastMessage.DocumentElement.ChildNodes[0].Name);
    Assert.AreEqual(1, out2Channel.LastMessage.DocumentElement.ChildNodes.Count);
    Assert.AreEqual("filter2", out2Channel.LastMessage.DocumentElement.ChildNodes[0].Name);
}

Lastly, we want to make sure the channel delivers copies of the original message instead of references to the same in-memory instance. We can do this by creating another filter, the ManglingFilter. A ManglingFilter modifies the message it receives from the channel. Our test makes sure that we can send a message to a ManglingFilter without our message object being harmed:

[Test]
public void ChannelMakesCopyOfMessage()
{
    StraightThroughChannel inChannel = new StraightThroughChannel();
    StraightThroughChannel outChannel = new StraightThroughChannel();

    new ManglingFilter(inChannel, outChannel);

    inChannel.Send(doc);
    Assert.AreEqual(0, doc.DocumentElement.ChildNodes.Count);
}

Channel Implementation

Now we are ready to implement a channel that supports all these test cases. The code is surprisingly simple because we use a local, i.e. distributable, channel:

public class StraightThroughChannel : IChannel
{
    OnMsgEvent subscriber;

    public StraightThroughChannel()
    {
        subscriber = new OnMsgEvent(NullEvent);
    }

    public void Subscribe (OnMsgEvent handler)
    {
        subscriber += handler;
       }

    public virtual void Send (XmlDocument msg)
    {
        subscriber((XmlDocument)msg.Clone());
    }

    private void NullEvent(XmlDocument doc)
    {}
}

Note the call to Clone that ensures we deliver a copy of the message that passed to the Send method. Actually, this implementation has a minor flaw. If multiple subscribers subscribe to the StraightThroughChannel , they each receive a copy of the original message. However, they receive the same copy. So if one subscriber mangles the message, other subscribers will see the change. If this is undesired, we have to replace the elegant one-liner delegate invocation with a foreach statement.

Code Download

The complete solution does not contain a lot of code but still a little more that I showed here. There is a SimpleFilter base class that does nothing but simply forward a message, I created an asynchronous channel and a Null Channel that acts like /dev/null for messages. You can download the Visual Studio solution here. Note that this solution requires nUnit 2.2.

Conclusion

Composability is a major ingredient into making system adaptable and promoting reuse. As simple as introducing a channel between two components may look, the devil is often in the detailed behavior. Defining a simple channel interface and specifying its behavior using coded unit tests is an important starting point to creating a composable architecture.

Our simple example used explicit conposition using code. One could easily imagine using a configuration file (more on configuration) or the new Visual Studio 2005 Software Factories.

Share:            

Follow:       Subscribe  SUBSCRIBE TO FEED

More On:  EVENTS     ALL RAMBLINGS   

Gregor is an Enterprise Strategist with Amazon Web Services (AWS). He is a frequent speaker on asynchronous messaging, IT strategy, and cloud. He (co-)authored several books on architecture and architects.