Building a Lean Modular Monolith with OSGi

While microservices are all the hype, notable experts warn against starting out that way. Instead you might want to build a modular monolith first, a safe bet if consider going into microservices later but do not yet have the immediate need. This article shows you how to build a lean monolith with OSGi, modular enough to be split into microservices without too much effort when that style becomes the appropriate solution for the application’s scaling requirements.

A very good strategy for creating a well-modularized solution is to implement domain driven design (Eric Evans). It already focuses on business capabilities and has the notion of bounded contexts that provide the necessary modularization. In this article we will use OSGi to implement the services as it provides good support for modules (bundles) and lightweight communication between them (OSGi services). As we will see, this will also provide a nice path to microservices later.

This article does not require prior knowledge of OSGi. I will explain relevant aspects as we go along and if you come away from this article with the understanding that OSGi can be used to build a decoupled monolith in preparation for a possible move towards microservices, it achieved its goal. You can find the sources for the example application on GitHub.

Our Domain: A Modular Messaging Application

To keep the business complexity low we will use a rather simple example – a chat application. We want the application to be able to send and receive broadcast messages and implement this in three very different channels:

  • shell support
  • irc support
  • IoT support using Tinkerforge based display and motion detector

Each of these channels uses the same interfaces to send and receive messages. It should be possible to plug the channels in and out and to automatically connect them to each other. In OSGi terms each channel will be a bundle and use OSGi services to communicate with the other channels.

Don’t worry if you do not have Tinkerforge hardware. Obviously the Tinkerforge module will then not work but it will not affect the other channels.

Common Project Setup and OSGi Bundles

The example project will be built using Maven and most of the general setup is done in the parent pom.

OSGi bundles are just JAR files with an enhanced manifest that contains the OSGi specific entries. A bundle has to declare which packages it imports from other bundles and which packages it exports. Fortunately most of this happens automatically by using the bnd-maven-plugin. It analyzes the Java sources and auto-creates suitable imports. The exports and other special settings are defined in a special file bnd.bnd. In most cases this file can be empty or even left out.

The two plugins below make sure each Maven module creates a valid OSGi bundle. The individual modules do not need special OSGi settings in the pom – for them it suffices to reference the parent pom that is being built here. The maven-jar-plugin defines that we want to use the MANIFEST file from bnd instead of the default Maven-generated one.

<build>
    <plugins>
        <plugin>
            <groupId>biz.aQute.bnd</groupId>
            <artifactId>bnd-maven-plugin</artifactId>
            <version>3.3.0</version>
            <executions>
                <execution>
                    <goals>
                        <goal>bnd-process</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>2.5</version>
            <configuration>
                <archive>
                    <manifestFile>
                        ${project.build.outputDirectory}/META-INF/MANIFEST.MF
                    </manifestFile>
                </archive>
            </configuration>
        </plugin>
        <!-- ... more plugins ... -->
    </plugins>
</build>

Each of the modules we are designing below creates an OSGi bundle. The poms of each module are very simple as most of the setup is already done in the parent, so we omit these. Please take a look at the sources of the OSGi chat project to see the details.

Declarative Services

The example uses Declarative Services (DS) as a dependency injection and service framework. This is a very lightweight system defined by OSGi specs that allows to publish and use services as well as to consume configuration. DS is very well-suited for OSGi as it supports the full dynamics of OSGi where bundles and services can come and go at any time. A component in DS can offer an OSGi service and depend on other OSGi services and configuration. Each component has its own dynamic lifecycle and will only activate when all mandatory dependencies are present. It will also dynamically adapt to changes in services and configuration, so changes are applied almost instantly.

As DS takes care of the dependencies the developer can concentrate on the business domain and does not have to code the dynamics of OSGi. As a first example for a DS component see the ChatBroker service below. At runtime DS uses XML files to describe components. The bnd-maven-plugin automatically processes the DS annotations and transparently creates the XML files during the build.

The Chat API

In our simple chat domain we just need one service interface, ChatListener, to receive or send chat messages. A ChatListener listens to messages and modules that want to receive messages publish an implementation of ChatListener as an OSGi service to signal that they want to listen. This is called the whiteboard pattern and is widely used.

public interface ChatListener {

    void onMessage(ChatMessage message);

}

ChatMessage is a value object to hold all information about a chat message.

public class ChatMessage implements Serializable {

    private static final long serialVersionUID = 4385853956172948160L;

    private Date time;
    private String sender;
    private String message;
    private String senderId;

    public ChatMessage(String senderId, String sender, String message) {
        this.senderId = senderId;
        this.time = new Date();
        this.sender = sender;
        this.message = message;
    }

    // .. getters ..

}

In addition we use a ChatBroker component, which allows to send a message to all currently available listeners. This is more of a convenience service as each channel could simply implement this functionality on its own.

@Component(service = ChatBroker.class, immediate = true)
public class ChatBroker {

    private static Logger LOG = LoggerFactory.getLogger(ChatBroker.class);

    @Reference
    volatile List<ChatListener> listeners;

    public void onMessage(ChatMessage message) {
        listeners.parallelStream().forEach((listener)->send(message, listener));
    }

    private static void send(ChatMessage message, ChatListener listener) {
        try {
            listener.onMessage(message);
        } catch (Exception e) {
            LOG.warn(e.getMessage(), e);
        }
    }

}

ChatBroker is defined as a declarative service component using the DS annotations. It will offer a ChatBroker OSGi service and will activate immediately when all dependencies are present (by default DS components are only activated if their service is requested by another component).

The @Reference annotation defines a dependency on one or more OSGi services. In this case volatile List marks that the dependency is (0..n). The list is automatically populated with a thread safe representation of the currently available ChatListener services. The send method uses Java 8 streams to send to all listeners in parallel.

In this module we need a bnd.bnd file to declare that we want to export the API package. In fact this is the only tuning of the bundle creation we do in this whole example project.

Export-Package: net.lr.demo.chat.service

The Shell Module

The shell channel allows to send and receive chat messages using the Felix Gogo Shell, a command line interface (much like bash) that makes for easy communication with OSGi. See also the appnote at enroute for the Gogo shell.

The SendCommand class implements a Gogo command that sends a message to all listeners when the command send <msg> is typed in the shell. It announces itself as an OSGi service with special service properties. The scope and function define that the service implements a command and how the command is addressed. The full syntax for our command is chat:send <msg> but it can be abbreviated to send <msg> as long as send is unique.

When Felix Gogo recognizes a command on the shell, it will call a method with the name of the command and send the parameter(s) as method arguments. In case of SendCommand the parameter message is used to create a ChatMessage, which is then sent to the ChatBroker service.

@Component(service = SendCommand.class,
    property = {"osgi.command.scope=chat", "osgi.command.function=send"}
)
public class SendCommand {

    @Reference
    ChatBroker broker;

    private String id;

    @Activate
    public void activate(BundleContext context) {
        this.id = "shell" + context.getProperty(Constants.FRAMEWORK_UUID);
    }

    public void send(String message) {
        broker.onMessage(new ChatMessage(id, "shell", message));
    }

}

The ShellListener class receives a ChatMessage and prints it to the shell. It implements the ChatListener interface and publishes itself as a service, so it will become visible for ChatBroker and will be added to its list of chat listeners. When a message comes in, the onMessage method is called and simply prints to System.out, which in Gogo represents the shell.

@Component
public class ShellListener implements ChatListener {

    public void onMessage(ChatMessage message) {
        System.out.println(String.format(
                "%tT %s: %s",
                message.getTime(),
                message.getSender(),
                message.getMessage()));
    }

}

The IRC Module

Continue reading %Building a Lean Modular Monolith with OSGi%


Source: Sitepoint