Introduction
In this episode we will learn how to assemble a subsystem that encapsulates functionality of a sub-domain of an e-commerce enterprise. The subsystem will be built on top of the Akka platform following a CQRS/DDDD-based approach. We will use the Akka-DDD framework, as it already implements concepts discussed previously, such as Aggregate Root
and Office
, and also provides other goodies (see the Readme page for the details).
The primary goal is to get familiar with the code structure of a concrete subsystem / service implementation before we take a deep dive into the topic of inter-service communication (business processes) in the next episode.
Subsystem components
As the architecture of the service adheres to the CQRS pattern, the subsystem will consist of the independent write- and read-side applications as presented on the following diagram:
On the write-side, the commands (in the form of HTTP POST requests) gets accepted by the write-front application and forwarded to the backend cluster (write-back applications) for processing. If a command succeeds, then the resulting event gets stored to the Event Store.
On the read-side, the queries (in the form of HTTP GET requests) gets accepted and executed against the View Store by the read-front application. The read-back application hosts the View Update Service, responsible for updating the View Store in reaction to the events streamed from the Event Store.
All applications can be started / restarted independently of each other.
The Sales Service
Let's checkout the code of the Sales Service, that is one of the services of a sample e-commerce system developed under the ddd-leaven-akka-v2 project.
The Sales
sub-domain of the e-commerce enterprise covers the preliminary phase of the Order Process
during which a customer adds or removes products to his/her shopping-cart and eventually confirms or cancels the order. To fulfill this functionality the Sales Service
exposes a Reservation
office.
The contract of the Reservation Office
The protocol/contract that the Reservation
office publishes to the outside world consists of commands (the Reservation
office is ready to handle) and events (the Reservation
office is writing to its journal) together with referenced, shared domain objects (such as Product and Money). All these classes are contained in the contracts module which other write- and read-side modules depend on. A client application (that wishes to send a command) or a service consumer (for example a Receptor
from another subsystem that subscribes to the Reservation
events) must adhere to the contract.
As different subsystems can get released / redeployed independently (which is a great advantage over the monolith system), changes in the contract of one service can break its consumers. Therefore in the long run, it is necessary to apply schema evolutions techniques, such as schema versioning or extensible serialization formats.
Akka-DDD supports json format as it is natively supported by the underlying Event Store provider. The currently used json library is Json4s. If you implement the commands and events as simple Scala case classes
(no polymorphic lists, no Scala's Enumeration, etc) you don't need to worry about serialization layer at all. If for some reason, you need to deal with such "extensions", don't forget to provide the serialization hints
by implementing and registering the JsonSerializationHintsProvider
class.
The office should also publish its identifier to be used across the system by the service consumers and client applications.
The office identifier should allow obtaining the identifier of the office journal and the identifiers of the journals of the individual clerks.
Akka-DDD provides the RemoteOfficeId class to be used for that purpose:
The identifier of the Reservation office is shown below:
The messageClass
property defines the base class of all message classes that the office is ready to handle (this information helps to auto-configure the command dispatching that is performed by the write-front application).
The Sales Service backend application [write-back]
The executable SalesBackendApp class, located in the write-back
module, starts the Sales
Actor System based on provided configuration file. The configuration must contain the following entries:
- entries that enable Akka Cluster capabilities,
entry that enables Cluster Client Receptionist extension,
entries that indicate the journal and the snapshot-store plugins.
The Cluster Client Receptionist extensions allows direct communication between actors from the write-front application and the backend cluster.
The startup procedure of the Sales Service
backend application is straightforward. First the Sales
Actor System joins the cluster using seed nodes. Addresses of the seed nodes are obtained from a file that is specified using the APP_SEEDS_FILE
environment variable. If the file is missing, the address is constructed from the app.host
(defaults to localhost
) and app.port
configuration entries. Then the Reservation
office gets created:
Assuming the newicom.dddd.cluster
package object is available in the scope of the startup procedure (the package object is imported), the actual office creation is delegated to the cluster-aware / sharding-capable OfficeFactory object that is injected automatically as an implicit argument of the office
method. The office factory requires a clerk factory and a shard allocation strategy to be implicitly provided for the given AggregateRoot (clerk) class. For the Reservation
these objects are defined in the SalesBackendConfiguration trait that the SalesBackendApp
mixes-in. The Office object that is eventually created contains the office identifier and the address (in the form of an ActorPath) of the office representative Actor.
The Reservation clerk (Aggregate Root)
Implementation of the Reservation clerk and the corresponding test is rather simple and self-explaining.
Please note that the office factory requires one more parameter to be implicitly provided for the given clerk class: the local office identifier (LocalOfficeId). This is an alternative form of the office identifier, prescribed to be used locally, within the write-back application. The local office identifier must indicate the class of the clerk, so the best place to define it, is the companion object of the clerk class (see Reservation#officeId).
The write-front application
Most of the building blocks of the write-front application is provided by the akka-ddd-write-front
module that is part of the Akka-DDD framework.
The Command Dispatcher
The Command Dispatcher is the core component of the write-front application. CommandDispatcher trait takes care of forwarding the incoming commands to the appropriate offices based on the provided remote office identifiers. The forwarding is performed using the Cluster Client (ClusterClientReceptionist
extension must be enabled).
The HTTP Command Handler
To make the offices available to the wide range of client applications, the write-front application should accept commands in the form of HTTP POST requests. The HttpCommandHandler is the component that implements all the steps of the command handling logic in the write-front application. First of all it takes care of unmarshalling the command from the incoming request. The request must contain Command-Type
attribute in its header, to indicate the class of the command that is passed in the request body. JSON is the expected format in which the command is encoded. Once the command is unmarshalled, the HTTP Command Handler passes it further to the Command Dispatcher. Eventually, once the command is processed on the backend and the response is received from the office (asynchronously), the handler converts it to an appropriate HTTP response that needs to be returned to the client.
The processing logic encapsulated in the HTTP Command Handler, is exposed as the Akka HTTP Route
object being the result of calling the handle
method:
The route returned by the HTTP Command Handler, is a building block of the complete route that needs to be implemented by the write-front application. Thanks to the Akka HTTP the complete http handler can be easily assembled. Just take a look at the route
method of the write-front HTTP Server of the Sales Service:
And that's all for now when it comes to the write side of the system. We didn't cover Receptors and Sagas which are to be presented in the forthcoming episode.
To test if the write side of the Sales Service is operating properly we can start the sales-write-back
and sales-write-front
applications (see the Wiki for detailed instructions) and send a CreateReservation
command. We will use httpie for this:
Hopefully you get the successful result (200 OK).
The Sales View Update Service [read-back]
The view side of the system is not that much interesting as the write side. Again, the logic of the processing is provided by the Akka-DDD. Please see a big picture of the View Update Service. In order to create a View Update Service for the SQL-based View Store provider, we need to extend the SqlViewUpdateService abstract class and provide simple configuration objects. The configuration object defines a sequence of projections for a given office (see: SalesViewUpdateService). The implementation of the projection is self-documenting:
The consume
method must return an instance of a parameterized ProjectionAction[E <: Effect]
which is a type alias of slick.dbio.DBIOAction[Unit, NoStream, E]
Projections can easily be tested using the in-memory H2 database. See the ReservationProjectionSpec.
The Reservation View Endpoint [read-front]
Finally we need to expose the views to the client applications via HTTP interface. This is a role of the read-front application. The HTTP server is implemented using Akka-HTTP in similar way as for the write-front application. The abstract route
method that is defined in the abstract ViewEndpoint must be implemented. The method takes viewStore
of type slick.jdbc.JdbcBackend
as an input parameter. The view is serialized using json format before it is returned to the client. Please see the implementation of the ReservationViewEndpoint. Note that the view access layer is reused from the read-back application.
After starting the sales-read-back
and sales-read-front
applications we should be able to fetch a view of the Reservation, that we created previously, using a http client:
"Microservices come in systems"
Since "One microservice is no microservice", in the next episode, we will see how to implement a business logic that can't be fulfilled by Sales Service alone. Stay tuned.