Architecture Module
This chapter will delve into the implementation of the Architecture Module within the HexArc framework.
Table of Contents
Implementation
The Architecture Module is the core module of the HexArc architecture, defining and implementing all the concepts required to describe, configure and deploy a service, following the best practices of the Hexagonal Architecture.
Below is the complete class diagram of the Architecture Module.

As shown in the diagram, the Architecture Module is made of two main submodules:
- Components Submodule: defines the concepts required to describe a service;
- VertxDSL Submodule: provides a DSL for configuring and deploying services in a declarative way.
As of now, HexArc supports only Vertx as the underlying framework for deploying services, but this isn’t a binding choice. In fact, some effort could be put in order to support other technologies (e.g. Akka…) in new modules for HexArc, abstracting the concepts shared among different modules.
Components Submodule
The Components Submodule is the part of the Architecture Module that defines the concepts required to describe a service, following the best practices of the Hexagonal Architecture.
Below is the class diagram of the Components Submodule.

The Components Submodule divides a service into components, called ServiceComponents, each taking care of providing or supporting the provision of part of the affordances of the service.
In order to define a ServiceComponent, you need to specify how it will be configured when initialized. In fact, the simplest ServiceComponent is just a function specifying its configuration.
The main ServiceComponents in a service are the following:
Ports: defines what are the affordances of the service with respect to a specific use case.Ports are parts of the contract of a service, as such they should be represented as traits. To address the concrete implementation of one or morePorts, HexArc adopts the term model.Adapters: typically defines how the affordances of the service are exposed to its users, enabling technologies for communicating with one or morePorts of the service. This is the case of inboundAdapters, which are the most common types. However, anAdaptercould also be used to monitor the service and to communicate its events to other services, which is the case of outboundAdapters.VertxService: it’s the service itself, or at least the part of the service holding together itsServiceComponents. In fact, thisServiceComponentis used internally for integrating all theServiceComponents of the service into a singleVerticlethat can be deployed within Vertx. Likely, it won’t be used by the end user, unless he requires for some reason to personalize how the integration between theServiceComponents happens.
Upon the deployment of a service, proper execution contexts, called ServiceComponentContexts, must be provided for all of its ServiceComponents. Then, each ServiceComponent is initialized consuming its corresponding ServiceComponentContext.
A ServiceComponentContext may contain all sorts of useful information for initializing a ServiceComponent. This information depends on the type of the ServiceComponent to initialize, but all ServiceComponentContexts provide:
name: the name of the service containing theServiceComponent;vertx: theVertxinstance on which the service is deployed;log: an Slf4jLoggerspecific to theServiceComponent.
More in detail, there are three types of ServiceComponentContext:
ServiceContext: theServiceComponentContextprovided when aVertxServiceis initialized, that is when its correspondingVerticleis deployed.PortContext: theServiceComponentContextprovided when aPortis initialized. It also contains theServiceContextof the service who owns thatPort.AdapterContext: theServiceComponentContextprovided when anAdapteris initialized. It also contains thePortexposed by thatAdapterand itsPortContext.
The Components Submodule provides the end user with all the means for instantiating a service, letting him personalize the integration of its ServiceComponents and the deployment of the service. However, since these processes can be complex, HexArc provides a DSL to make things easier, less imperative and more explicit.
VertxDSL Submodule
The VertxDSL Submodule is the part of the Architecture Module that defines the DSL for configuring and deploying services in a declarative way, without the hassle of manually integrating their ServiceComponents.
Below is the class diagram of the VertxDSL Submodule.

The entrypoint of the module is the DSL, modelled by the object VertxDSL. The VertxDSL consists in a set of exports defining the vocabulary of the DSL and enriching its syntax by means of different extensions.
In particular, the vocabulary of the DSL is defined by the VertxDSL.Vocabulary, which exports the different contexts of the DSL, called DSLContexts, as keywords of the DSL.
A DSLContext defines which keywords of a DSL are or aren’t allowed inside the portion of code within the DSLContext. In fact, a DSLContext may be a DSLContext.Root, meaning that its corresponding keyword can be used anywhere, or a DSLContext.Child, meaning that its corresponding keyword can only be used within its parent DSLContext.
For example, referring to the User Documentation, an Adapter may be defined only within a Port and it cannot be defined as a direct child of a Service. In order to explicit when a context is closed, the example also reports the end new scala syntax, which is completely optional.
new DeploymentGroup(Vertx.vertx()): // opening "DeploymentGroup" DSLContext (Root)
new Service: // opening "Service" DSLContext (Child)
name = "ColoredLampService"
new Port[LampSwitchPort]: // opening "Port" DSLContext (Child)
name = "SwitchPort"
model = ColoredLampModel()
// Here the keyword `Adapter` exists
new Adapter(LampSwitchHttpAdapter()): // opening "Adapter" DSLContext (Child)
name = "Http"
end new // closing "Adapter" DSLContext
end new // closing "Port" DSLContext
// Here the keyword `Adapter` does not exist
new Adapter(LampSwitchHttpAdapter()): // ERROR: keyword `Adapter` is not inside a `Port`
name = "Http"
end new
end new // closing "Service" DSLContext
end new // closing "DeploymentGroup" DSLContext
Note: here
DeploymentGroup,Service,Port,Adapter,nameandmodelare the keywords of the DSL. In particular, the keywordsPortandAdapterare not the same classes as their homonyms in the Components Module.
As introduced by the example above, HexArc defines four main types of DSLContext:
-
DeploymentGroupDSLContext: aDSLContext.Rootdescribing the configuration for the deployment of a group of services. Such configuration consists of a list of the services that should be deployed. These services can then be deployed by callingdeployon theDeploymentGroupDSLContext.The
VertxDSL.Vocabularyexposes this type ofDSLContextas the global keywordDeploymentGroup. While aDeploymentGroupDSLContextexposes the followingServiceDSLContextas the scoped keywordService. The actual implementation relies on the definition of a type member calledService.Note: in HexArc, a global keyword is a keyword that can be used everywhere in the code, therefore it should be made available everywhere in the code; while a scoped keyword is a keyword that requires positioning inside a specific scope, therefore it should be made available only in the scope where it is allowed to use it.
-
ServiceDSLContext: aDSLContext.ChildofDeploymentGroupDSLContextdescribing the configuration for the instance of a service to be deployed. Such configuration consists of a list of thePorts forming the contract of the service.When a
ServiceDSLContextis opened (i.e. created) within aDeploymentGroupDSLContext, it automatically adds itself to the services that should be deployed by thatDeploymentGroupDSLContext.The
VertxDSL.Vocabularyexposes this type ofDSLContextas the global keywordService, so that it could be lazily configured outside aDeploymentGroupDSLContext. While aServiceDSLContextexposes the followingPortDSLContextas the scoped keywordPort. -
PortDSLContext: aDSLContext.ChildofServiceDSLContextdescribing the configuration of aPortof a service. Such configuration includes the contract exposed by thePort(defined as type parameter), the actual implementation of thePortand a lists of theAdapters installed on thePort.When a
PortDSLContextis opened within aServiceDSLContext, it automatically adds itself to thePorts of the service configured by thatServiceDSLContext.The
VertxDSL.Vocabularyexposes this type ofDSLContextas the global keywordPort(not to be confused with the homonym component), so that it could be lazily configured outside aServiceDSLContext. While aPortDSLContextexposes the followingAdapterDSLContextas the scoped keywordAdapter(still, not to be confused with the homonym component). Moveover, it exposes the scoped keywordmodelfor configuring its actual implementation. -
AdapterDSLContext: aDSLContext.ChildofPortDSLContextdescribing the configuration of anAdapterof aPortof a service. Such configuration consists of the implementation of theAdapter.When an
AdapterDSLContextis opened within aPortDSLContext, it automatically adds itself to theAdapters of thePortconfigured by thatPortDSLContext.The
VertxDSL.Vocabularyexposes this type ofDSLContextas the global keywordAdapter(not to be confused with the homonym component), so that it could be lazily configured outside aPortDSLContext, provided the type ofPorton which it can be installed.
These last three DSLContexts are extended using a mixin called NamedDSLContext, which exposes a new scoped keyword for each them, called name, for configuring the name of each ServiceComponent (used for creating their Loggers).
As the DSLContexts of the VertxDSL are opened, a tree-like structure is generated starting from the root, which is always a DeploymentGroupDSLContext.
Internally, each of the four types of DSLContext provide a close method, which is used to finalize their configuration. In particular, when deploy is called on a DeploymentGroupDSLContext, all of the DSLContexts belonging to its tree are closed bottom-up, configuring the corresponding ServiceComponents. Finally, the DeploymentGroupDSLContext closes itself, deploying the configured services.
The deployment of the services is delegated to a support class called Deployment. Its companion object provides methods for deploying services and obtaining their corresponding Deployments, while an instance of the Deployment class itself allows to undeploy the corresponding service.
In addition to the DSLContexts, another way the VertxDSL enriches its syntax is by means of extensions. In particular, it exports all the extension methods provided by the VertxDSLExtensions object, which include methods for manipulating Futures (e.g. awaiting the deployment or un-deployment of a service…).
To summarize, the VertxDSL is defined through keywords, where a global keyword can be either:
- a
DSLContext, exposing new scoped keywords in the form of:- public or protected methods;
- public or protected type members.
- an extension method provided by some extension of the DSL.
From Functional DSL to YAML-like DSL
Initially, HexArc provided a functional DSL (where keywords were pure functions), instead of the current YAML-like DSL (mostly based on anonymous classes).
One of the reasons why HexArc migrated from its original functional syntax to a YAML-like syntax is type inference. For example, inside a
Portkeyword, theAdapterscoped keywords automatically refer to the proper type ofAdapterfor thatPort, without requiring the user to explicit that type for eachAdapter.// Original functional syntax deploy(Vertx.vertx()){ service("CustomLamp"){ port[DimmableLampPort](DimmableLampModel()){ // Here `[DimmableLampPort]` couldn't be omitted adapter[DimmableLampPort](DimmableLampLocalAdapter()) adapter[DimmableLampPort](DimmableLampHttpAdapter()) adapter[DimmableLampPort](DimmableLampMqttAdapter()) } port[ColoredLampPort](ColoredLampModel()){ // Here `[ColoredLampPort]` couldn't be omitted adapter[ColoredLampPort](ColoredLampLocalAdapter()) adapter[ColoredLampPort](ColoredLampHttpAdapter()) adapter[ColoredLampPort](ColoredLampMqttAdapter()) } } }// Current YAML-like syntax new DeploymentGroup(Vertx.vertx()): new Service: name = "CustomLamp" new Port[DimmableLampPort]: model = DimmableLampModel() // Here `Adapter` can only mean `Adapter[DimmableLampPort]` new Adapter(DimmableLampLocalAdapter()) new Adapter(DimmableLampHttpAdapter()) new Adapter(DimmableLampMqttAdapter()) new Port[ColoredLampPort]: model = ColoredLampModel() // Here `Adapter` can only mean `Adapter[ColoredLampPort]` new Adapter(ColoredLampLocalAdapter()) new Adapter(ColoredLampHttpAdapter()) new Adapter(ColoredLampMqttAdapter()) }Other reasons of the migration include the following:
- A YAML-like syntax feels more proper for representing a data structure, such as the configuration of the deployment of a group of services. In that sense, it is also more direct to extend the DSL without reducing its readability (e.g. just add a method for configuring a field in a
DSLContextand it won’t affect how the DSL visually appears…).- A YAML-like syntax provides support for scoped keywords. All the keywords of a functional DSL are available everywhere in the code, even though some require positioning within a specific scope. By defining keywords as type members instead of functions, it is possible to make keywords available only in the scopes where they can actually be used.
Of course the YAML-like syntax comes with its own downsides, the most noticeable being that the functional syntax still appears cleaner, as it does not require the boilerplate code that a YAML-like syntax does require (e.g. as of now, the
newkeyword is unfortunately mandatory for creating anonymous classes in Scala 3…).