Skip to content

architecture microservice anatomy

felix-b edited this page Oct 12, 2016 · 5 revisions

NOTE: This page is still being written

DISCLAIMER: This documentation refers to milestone Boda, which is currently in early stages of development. Some or all of the features mentioned here may not yet exist, or be unstable.

Microservice anatomy

This page explains the internal design of microservices - that is, what's going on inside a microservice.

Technology abstraction

An important goal of microservice internal design is technology abstraction. That is, letting application code be written with no regard to any concrete technology stack. Application code must be totally unaware of things like SQL, Entity Framework, NHibernate, MongoDB, EventStore, ASP.NET, WPF, WCF, MSMQ, Socket API, etc etc - you get the idea.

Technology abstraction provides valuable benefits. In addition to the ability of transparently switching to a different technology (e.g. replacing a relational database with a No-SQL engine), which usually never happens, the more important benefits are:

  • Reuse and DRY. Business capabilities and use cases of the application are clearly decoupled from the interaction channels through which they are invoked. This allows seamlessly expose those capabilities through new interaction channels (e.g. UI vs. API), or reuse them in new workflows in the ways that were not initially anticipated.

    • Without the above decoupling, for example, a use case that was initially programmed to be performed through user interface, usually cannot be easily exposed as an API for external systems, when such a requirement arrives later - often resulting in additional rework, or even worse, in code duplication. The latter also violates the DRY principle, accelerating the codebase towards Big Ball of Mud.
  • Testability. Application code can be easily driven by automated tests in isolation from databases, message queues, external services, and user interface. This enables writing unit and component tests, which otherwise would be tricky or impossible. Moreover, integration and system tests can exercise numerous use cases and scenarios while bypassing the user interface, which significantly simplifies creation and maintenance of such tests. The automated UI tests are still important, but once they don't need to cover the entire variety of internal use cases, they can be shrunk to a much smaller size.

  • Expertise homogeneity. It's about letting the right people do the right job. The details of using a concrete technology are encapsulated in an adapter module, which is authored by people in the community, who have proper expertise in that specific technology. For application developers, the effort of incorporating a new technology in the application is reduced to including the right module in a microservice. In this way, the application developers aren't required to be skilled in every technology actually used in the application; instead, they should be experts in using application frameworks like DDD and UIDL, which is one skill uniformly required for development of many different applications.

    • Traditionally, the variety of technologies used by an application requires having people on board with proper expertise in many different areas - which is a headache. Acquiring full-stack developers experienced with exactly the right set of technologies can be hard. Alternatively, extending team with professionals in different technologies can become very expensive, bearing in mind that every additional team member doubles the coordination overhead. Yet another way, is letting existing team members learn every new technology used in the project. While the latter can be very attractive to the team (or may be not), the learning curve is expensive, and technical risks of improperly using the newly learnt technology arise. Another bad thing is abandoning the use of a new technology for the above reasons, despite the fact that it is a perfect fit for the problem at hand.

Hexagonal architecture

In order to implement the technology abstraction requirement, microservices follow Hexagonal architecture, as illustrated in the figure below.

Hexagonal architecture

According to Hexagonal architecture, a microservice is structured as follows, in the top-down order:

  • Application and building block domains
    • depend only on frameworks
    • unaware of any concrete technology.
  • Frameworks (the blue and the red pieces in the figure)
    • upwards, provide programming models for the application and the building block domains
    • downwards, depend on abstract technology stack adapters.
  • Technology stack adapters (the brown pieces in the figure)
    • upwards, implement abstractions on which the frameworks depend
    • downwards, interact with the concrete technology.

Capsules

Capsules are the runnable units of microservices.

A capsule is a logical construct, which allows using unified terminology and common implementation of microservice hosts, regardless of their run mode (batch vs. daemon), deployment (server side vs. fat client), and process mapping (process dedicated to a single unit vs. process shared by multiple units).

One capsule hosts one replica of one instance of one microservice. Instances of batch microservices are said to run each as a single replica, for the sake of common terminology.

Basically, a capsule consists of a DI container, a loader, and a list of modules.

  • Loader is responsible for loading and initializing modules in the defined order (explained in detail later on this page).
  • DI container is the place where modules contribute their components and lookup other components they depend on.
  • Modules plug in new functionality, or override functionality plugged in by far, by contributing their components to the DI container.

On the figure below: a process shared by two microservice capsules.

Microservice capsules

Modules

A capsule has no knowledge of what functionality it is going to provide. The functionality of a microservice is completely determined by the list of modules loaded into the capsule. It is also possible to have different list of modules for different instances of a microservice.

A module represents a pluggable, logically complete piece of functionality, an infrastructural framework or a business capability, or a set of closely related capabilities. A module is usually a class library, or a set of class libraries.

The modules are plugged in by contributing one or more of their components to DI container of the capsule, as shown in the figure below:

Modules and contributed components

Some of contributed components add up to functionality already plugged in, while the others override it. For this reason, the order in which modules are loaded is important. The pluggability of modules allows flexible separation of subsystems and customizations. For example, customer-specific, vertical-specific, or region-specific deployments can be achieved simply by tweaking the list of modules to load.

Components

Components are classes which encapsulate a more-or-less significant unit of functionality, or a polymorphic strategy.

Conceptually, components can be classified as policies, dependencies, or a combination of both. While dependencies are just regular components, on which other components depend in order to fulfil their responsibilities, policies need to be explained in more detail.

Policies

A policy is a component, which has policy-mechanism relation with one or more frameworks, where frameworks are the mechanism. The combination of a policy component applied on top of a framework mechanism provides a working piece of functionality.

Technically, one component can encapsulate multiple policies for multiple frameworks, and at the same time serve a dependency for other components. However, this ability should be applied carefully, with the single responsibility principle in mind.

Conventions

Frameworks define conventions for their policy components, in order to be able to pick them from DI container and communicate with them. The conventions include (but are not limited to): implementing or inheriting framework-defined base types, applying framework-defined attributes, and naming.

Often, multiple alternative conventions exist, which result in the same semantics, so that an application can choose style of convention that fits best (e.g., applying attributes vs. naming convention).

Code generation

Depending on the framework and its conventions, policy components can be concrete classes, partially implemented abstractions, or pure abstractions, i.e. interfaces. According to conventions, a framework can implement abstract members, decorate existing members with additional aspects, and implement additional infrastructural interfaces. This process is called late compilation. TODO: provide link to late compilation page.

Example of policy component

Diagnostics framework defines a convention of logger. A logger is an interface, which (among the other features we leave out here to keep our example simple) declares a method for every log message. The methods can have parameters for the log messages they represent. When a method is invoked, it sends a log message after its name, with parameters included, to subscribed logging targets.

Such formalized approach to logging greatly simplifies log analysis and metric collection.

Application modules declare loggers according to their needs, and contribute them to DI container. Contributed logger interfaces are then picked up by the Diagnostics framework, and automatically implemented by conventions.

Next, application code obtains implemented loggers by their interfaces, through dependency injection. Calls made on the logger objects result in log messages being actually written to logging targets.

An example logger component is listed below:

public interface IAccountTransactionLogger
{
    void InfoFundsWithdrawn(string accountId, decimal amount, decimal newBalance);
    void ErrorFundsInsufficient(string accountId, decimal amount, decimal currentBalance);
}

The same logger declared according to an alternative convention:

[Diagnostics.Logger]
public interface IAccountTransactionLogger
{
    [Diagnostics.Info]
    void FundsWithdrawn(string accountId, decimal amount, decimal newBalance);
    [Diagnostics.Error]
    void FundsInsufficient(string accountId, decimal amount, decimal currentBalance);
}

Module loader

A module contributes its components by registering them in the DI container of the capsule. This happens in a module loader, which is a class that implements IModuleLoader interface. A module should have exactly one module loader class.

Module loaders can apply logic and read configuration in order to determine whether and what concrete type of a component should be contributed.

However, if not special logic applies, components can be discovered automatically, which reduces the burden and the chance of forgetting to contribute a newly written component.

The automatic discovery mechanism is an infrastructural implementation of IModuleLoader. To enable automatic discovery in a module, the module should contain an empty module loader class which inherits AutoDiscoveringModuleLoader. Frameworks need to declare their discovery conventions in order to enable automatic discovery for their policy components.

More details can be found in Application composition.

Feature loaders

Sometimes modules are big and contain multiple functionalities. In such case, a module doesn't has to be a monolith. It can be divided into features. When an application includes a module in a microservice, it is possible to specify what features from the module are included.

Feature loaders are components that must be contributed by module loader. They can also be automatically discovered.

Feature loaders work by the same principle as module loaders. Feature loader classes must implement IFeatureLoader interface, and AutoDiscoveringFeatureLoader class exists for automatic discovery of feature components. Note that automatic discovery of feature components is scoped to namespace of feature loader and its sub-namespaces.

More details can be found in Application composition.

Component lookup

Traditionally, the components are looked up by services they implement, that is, interfaces or base classes. A component can be registered for multiple services.

Each service is applied one of three lookup styles, which determine lookup and override behavior of the DI container in regards to the service. Override behavior applies when a module registers a component for a service, then another module loaded later in order, registers a different component for the same service.

  • Single lookup: one component is returned from container. If multiple components were registered, rules of precedence are applied.
  • Set lookup: a collection is returned, containing all of the components registered for the service, in no specific order.
  • Pipeline lookup: a collection is returned, containing all of the components registered for the service, according to the ordering requests (e.g. being the first or the last one) applied to component registrations.

More details on the lookup styles and override behaviors can be found in Application composition.

Purpose groups of modules

Technically, the capsule just loads modules from the list, and all modules are treated the same. However, different modules have different purposes, and it is beneficial to distinguish different groups of modules and define their load order accordingly.

Next sections explain each of module groups and their purposes, in the bottom-up order.

Module group Naming scheme Purpose
Kernel/Platform facilities NWheels.Kernel.*, NWheels.Platform.* These modules contribute crucial pieces of low-level infrastructural functionality from the Kernel and Platform layers. They include such frameworks as daemon lifecycle, meta-type system, and scalability & availability.
Technology stack adapters NWheels.Stacks.<area>.<technology> These modules are adapters of technology-independent ports to concrete technologies, according to Hexagonal architecture. For example, a data persistence port in DDD framework can be adapted to work with MongoDB database engine by a module named NWheels.Stacks.DB.Mongo.
Application frameworks NWheels.Frameworks.* These modules contribute high-level application frameworks like DDD and UIDL. Only frameworks used by upstream modules should be included.
Building block domains NWheels.Domains.* These modules contribute adaptable domain models and logic for common infrastructural and business domains like CRM, E-Commerce, DevOps, and many more. Building-block domains allow quickly incorporate ready implementations of entire areas into the application. Two infrastructural domains which are typically included in every application, are NWheels.Domains.Security and NWheels.Domains.DevOps.
Application modules <app>.** These are modules that contain application-specific functionality, adaptation of reused building block domains, and implementation of unique features. Multiple application modules may represent the core and the different subsystems of the application. For applications that support customized versions, this module group only contains the white-label portion of the software.
Customization modules <app>.Customizations.**, <app>.Integrations.** These modules specialize application functionality in different aspects, e.g. verticals, regions, customers, or integrations with different external service providers. This purpose group exhibits the most variability. Typically, customized deployments are obtained just by tweaking the list of customization modules.

Background

Architecture

Feature Explorer

Platform Frameworks

  1. Kernel Layer
  2. Platform Layer
  3. Scalability & Availability Layer

Application Frameworks

  1. Domain-Driven Design
  2. Processing Workflows
  3. User Interface
  4. Data representation
  5. Semantic Logging & Data Collection
  6. Testing

Building Block Domains

  1. Infrastructure Domains
  2. Business Domains

Technology Stacks

  1. Database
  2. User Interface
  3. Communication Endpoints
  4. Scalability
  5. Services/Libraries

Developer Tools & Resources

  1. Developer Tools
  2. nwheels.org powered by NWheels
  3. Popular communities
  4. Books and videos

Contributors

Clone this wiki locally