In this article I am going to present a realistic example that will show you how to organize your code and how to describe this organization using our architecture DSL. Let us assume we are building a micro-service that manages customers, products and orders. A high level architecture diagram would look like this:
It is always a good idea to cut your system along functionality, and here we can easily see three subsystems. In Java you would map those subsystems to packages, in other languages you might organize your subsystem into separate folders on your file system and use namespaces if they are available.
Let us assume the system is written in Java and its name is "Order Management". In that case we would organize the code into those 3 packages:
com.hello2morrow.ordermanagement.order com.hello2morrow.ordermanagement.customer com.hello2morrow.ordermanagement.product
This can easily be mapped to our DSL:
artifact Order { include "**/order/**" connect to Customer, Product } artifact Customer { include "**/customer/**" } artifact Product { include "**/product/**" }
Internally all three subsystem have a couple of layers and the layering is usually the same for all subsystems. In our example we have four layers:
A service would only expose its service ad its model layer to the outside. The service layer contains all the service interfaces and talks to the controller and the model layer. The controller layer contains all the business logic and uses the data access layer to retrieve or persist data using JDBC. The model layer is defining the entities we are working with.
We will use a separate architecture file named "layering.arc" to describe our layering:
// layering.arc artifact Service { include "**/service/**" connect to Controller } artifact Controller { include "**/controller/**" connect to DataAccess } require "JDBC" artifact DataAccess { include "**/data/**" connect to JDBC } public artifact Model { include "**/model/**" } interface IService { export Service, Model }
Please note, that we declared "Model" as a public artifact. That saves us the need to explicitly connect all the other layers to "Model". Also note the "require" statement. Here refer to a third architecture file, that contains the definition of the artifact JDBC. This way we can ensure that only the data access layer can make JDBC calls. using "require" will only declare the artifacts contained in the required file, but not define them. This means that the artifacts in "JDBC" have to be defined on another level. The interface is used to define the exposed parts of a subsystem. When connecting to the "IService" interface you have only access to the "Service" and the "Model" layer.
NOTE
Architecture files using "require" are not self-contained and cannot be added to the architecture check!
Now we use apply statements to apply the layering to our three subsystems:
artifact Order { include "**/order/**" apply "layering" // Connect to the IService interface of Customer and Product connect to Customer.IService, Product.IService } artifact Customer { include "**/customer/**" apply "layering" } artifact Product { include "**/product/**" apply "layering" } // By using apply we define the artifacts of "JDBC" in this scope apply "JDBC"
We also apply "JDBC" in the outermost scope to ensure that the artifacts in there are defined exactly once.
For the sake of completeness, here is the definition of "JDBC.arc":
// JDBC.arc artifact JDBC { include "**/javax/sql/**" }
By using smart package naming it becomes easy to map your code to the architecture description. For example the order subsystem would have four packages:
com.hello2morrow.ordermanagement.order.service com.hello2morrow.ordermanagement.order.controller com.hello2morrow.ordermanagement.order.data com.hello2morrow.ordermanagement.order.model
As you can see it required relatively little effort to create a formal and enforceable architecture description for our example.