Skip to main content

2 posts tagged with "microservices"

View All Tags

Designing and Implementing Microservices with DDD and Hexagonal Architecture

· 12 min read
Reda Jaifar
Lead Developer

Use case

In this phase, we begin by collecting requirements from a business perspective to ensure a thorough understanding of the objectives. Once these requirements are established, we then investigate how architectural patterns and software development methodologies can be utilized to construct the product effectively.

Currently, local grocery stores must either visit large distribution companies or contact them individually to inquire about pricing, stock availability, and delivery schedules when placing orders. This process is time-consuming and inefficient for grocery managers. To address this challenge, we propose developing a platform that enables managers to easily check prices, view stock levels, place orders, and track deliveries in one streamlined solution.

Personas and User Journey

To develop a system that effectively tackles the relevant challenges and provides significant value, it is imperative that every aspect of the system is designed with users in focus. This makes it vital to thoroughly understand their frustrations, motivations, and expectations regarding the new product we are creating

  • Grocery Manager
    • Profile: Typically has a bachelor's degree.
    • Frustrations: Running out of stock, lack of visibility over deliveries, price comparison is a time-consuming task.
    • Motivations: Save management time, focus on customer relationships and service quality, ensure stock levels are always adequate.

Architecture & Design

In the previous section, we presented a brief overview of the business domain, requirements, and one of the system's future users. At this stage, we need to make architectural and design decisions.

Architectural Style

While the brainstorming outputs for this example are somewhat limited, we can easily envision various services, such as Order Service, Provider Service, Delivery Service, and Product Service. Now, suppose we want to develop all these services simultaneously, with different teams specializing in distinct technology stacks. It is essential that these services remain decoupled from one another, ensuring that a change in the design of one service does not affect the others.

Regarding deployment, we aim to deploy the Product Service before the others to deliver value as quickly as possible. This necessitates that the services be independently deployable and scalable.

By organizing our product around services, we can enhance fault tolerance. For example, if the Delivery Service is unavailable, users should still be able to access other services. For these reasons, we believe that a microservices architecture is the most suitable architectural style for this project.

Regarding deployment, we aim to deploy the Product Service before the others to deliver value as quickly as possible. This requires that services be independently deployable and scalable.

Domain Design

The complexity of the business domain, the rules, and the number of services (system components) lead us to consider Domain-Driven Design (DDD). So, what is DDD?

info

DDD, as described in the excellent book Domain-Driven Design by Eric Evans (Addison-Wesley Professional, 2003), is an approach to building complex software applications that is centered around developing an object-oriented domain model.
A domain model captures knowledge about a domain in a form that can be used to solve problems within that domain.

In traditional object-oriented design, a domain model is a collection of interconnected classes. For example:

Figure 1

Figure 1: Object Oriented Domain Model

With this design, operations such as loading or deleting an Order object encompass more than just the Order itself; they also involve related data, such as order items and delivery details. The absence of clear boundaries complicates updates, as business rules, imagine a business logic such as "minimum order amounts" must be enforced meticulously to preserve invariants

This is where DDD can help, by using Aggregates

An aggregate is a cluster of domain objects within a boundary that can be treated as a unit

Figure 2 shows a simplified version of the domain model aggregates. Designing domain model using the DDD Aggregate pattern recommand that aggregates match a set of rules: 1. Reference only the aggregate root 2. Inter-aggregate references must use primary keys 3. One transaction creates or updates one aggregate

Figure 2

Figure 2: Domain Model Aggregates Simplified

Implementation

Now that we have designed the domain model aggregates and the achitecture implementation view Microservices, let's dive into the architecture logical view Hexagonal Architecture , please refer to this post to learn about Architecture Implementation view, Hexagonal option and why we adopt it

I'm going to use Kotlin as programming language and maven as a build tool

Pilot feature

Bootstrap the project

Let's consider the following feature: place order

As a grocery manager, I need to place an order for a product so I can provision the stock Constraint 1: The minimum order quantity is 20.

Using maven build tool, let's create a project for kotlin.

  1. create a maven project using the following cmd:
    mvn archetype:generate \
-DgroupId=com.algodema \
-DartifactId=grocery-marketplace-order-service \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=false
  1. convert the project to kotlin by replacing the src/main/java and src/test/java directories with src/main/kotlin
  2. modify pom.xml for Kotlin, JUnit, and AspectJ
  3. checkout the project on github: (https://github.com/algodema/microservices-labs/tree/main/grocery-marketplace-order-service)

Implement the place order feature using TDD

What about starting with writing a test scenario for our uese case using TDD (Test Driven Development), this approach help me immediately implement business logic, in this case ensure no order will be created with quantity less than 20 unit.

    @Test
fun `should not place order with quantity lower than 20`() {

val productId: ProductId = ProductId(UUID.randomUUID())
val providerId: ProviderId = ProviderId(UUID.randomUUID())
val quantity: Int = 16

val orderCreated = placeOrder.invoke(productId, providerId, quantity)

Assertions.assertEquals(orderCreated.state, OrderState.PLACED)
}

I started by creating "PlaceOrderFeatureTest" test class, then writing my first test as we see in the snippet above. If we look at the project structure in figure below: Figure 3

note

Within the domain package, I created two sub-packages: features and models. The models sub-package includes representations of key domain entities, such as Order, OrderId, ProductId, and OrderState. Meanwhile, the features sub-package contains feature-specific classes, with PlaceOrderFeature being the current implementation. It's important to note that in the models package, we organize classes according to their respective business domains.

The approach involves declaring instances of productId, providerId, and placeOrder prior to the existence of their respective classes. Subsequently, these classes are developed and organized into packages in accordance with the principles of hexagonal architecture, which positions business logic at the core, as represented by the domain package.

We continue writing tests, they should be fixed and failed as we implement the business requirements in our place order feature.

Infrastructure implementation

Now that we created our first feature, we would like to expose it through a REST API endpoint, but also persist the created Order in a storage, for the purpose of this tutorial, we will implement a in-memory persistence.

The hexagonal architecture defines ports and adapters as interfaces and implementations consequentially used to make the domain interacting and connecting with other components of the application such as (persistency, api, messaging, ...)

note

A port defines a set of operations that facilitate interaction between business logic and external systems. In our Kotlin example, these ports are represented by Java/Kotlin interfaces. An adapter manages requests from external sources or from the business logic itself by invoking external applications or services, such as databases or message brokers. Both ports and adapters can be categorized as inbound or outbound to distinguish between requests directed toward the business logic and those initiated by it.

Ports destination packages

Ports will reside in the same root package as domain because they are integrated part of it. For our example: com.algodema.grocery.markeplace.domain.ports As mentioned before, we separate them into 2 distinct sub packages:

  • com.algodema.grocery.markeplace.domain.ports.inbound
  • com.algodema.grocery.markeplace.domain.ports.outbound

Where adapters reside in the infrascture root package that we create to group all infrastcutures adapters such as:

  • REST API controllers classes
  • InMemory, Postgres or any other Repository implementations that serve to persist data.
  • External Systems integration such as SAPClient for example.

Let's create the follwing ports and adapters:

  1. OrderRepository as an outbond port.
  2. PlaceOrder as inbound port.
  3. OrdersApi as inbound adapter that will use PlaceOrder port to expose the feature as REST API endpoint.
  4. InMemoryOrderRepostory as an outbound adapter that will implement the OrderRepository port interface.

Below we created the InMemoryOrderRepository class that implements the domain port OrderRepository interface, Note also that we annotate this class with the Spring framework @Repository in order to make it discoverable by Spring IoC container. Remember that we use Spring at the infrastcuture level without any coupling with the domain.

    package com.algodema.grocery.marketplace.orderservice.infrastructure.spi

import com.algodema.grocery.marketplace.orderservice.domain.models.order.Order
import com.algodema.grocery.marketplace.orderservice.domain.ports.outbound.OrderRepository
import org.springframework.stereotype.Repository

@Repository
open class InMemoryOrderRepository: OrderRepository {
override fun save(order: Order): Order {
throw NotImplementedError("not yet implemented")
}
}

Next, we will introduce the Spring framework at the infrastructure layer to create a REST API. We rely on the Spring framework's dependency injection to make our component connections decoupled. Using Dependency Injection, the place order feature will hold an instance of OrderRepository to save the created order, and at the infrastructure's API adapter, the REST Controller will hold instances of our features by dependency injection as well.

note

This is where Hexagonal Architecture shines. We can replace Spring by any other framework for exposing REST APIs or handling persistence without modifying the code within our domain. This decoupling keeps the domain safe, adaptable, and maintainable, allowing us to change or add new business rules independently of the infrastructure. For example, if we decide to switch to the Quarkus framework because it is better suited for cloud-native environments, the domain remains completely unaffected.

To enable Spring to identify our features for dependency injection, we will create a new root package designated as ddd. This package will encompass the necessary annotations:

  1. Feature Annotation: marks our features classes
    package com.algodema.grocery.marketplace.orderservice.ddd

@Retention(AnnotationRetention.RUNTIME)
annotation class Feature()

After creation, we utilize the Feature annotation to designate our place order functionality accordingly.

    @Feature
class PlaceOrder(private val repository: OrderRepository) : PlaceOrder {
// ...
}
tip

As previously noted, we will be utilizing Spring Boot for this project. Therefore, it is essential to incorporate the Spring Boot and Spring Web dependencies into our project, as well as to include the Spring Boot Maven plugin within the Maven build plugins.

Let's create the OrdersApi in the infrastructure package under the sub package api, as follow:

    package com.algodema.grocery.marketplace.orderservice.infrastructure.api

import com.algodema.grocery.marketplace.orderservice.domain.features.PlaceOrder
import com.algodema.grocery.marketplace.orderservice.domain.models.order.Order
import com.algodema.grocery.marketplace.orderservice.domain.models.product.ProductId
import com.algodema.grocery.marketplace.orderservice.domain.models.provider.ProviderId
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/orders")
class OrderServiceApi(private val placeOrder: PlaceOrder) {

@PostMapping
fun placeOrder(@RequestBody placeOrderRequest: PlaceOrderRequest): Order {

val productId: ProductId = ProductId.from(placeOrderRequest.productId)
val providerId: ProviderId = ProviderId.from(placeOrderRequest.providerId)
val quantity: Int = placeOrderRequest.quantity

return placeOrder.invoke(productId, providerId, quantity)
}

}

We now need to configure Spring to recognize our annotated features, enabling them to be loaded into its bean container. To achieve this, we will create a configuration class within a subpackage named config under the infrastructure package. Below is our configuration class:

    package com.algodema.grocery.marketplace.orderservice.infrastructure.config

import com.algodema.grocery.marketplace.orderservice.ddd.Feature
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.FilterType

@Configuration
@ComponentScan(
basePackages = ["com.algodema.grocery.marketplace.orderservice"],
includeFilters = [ComponentScan.Filter(
type = FilterType.ANNOTATION,
value = [Feature::class]
)]
)
open class DomainInjectionConfig

The final step is to transform our application's entry point class into a Spring Boot application as follows:

    package com.algodema.grocery.marketplace.orderservice

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication


@SpringBootApplication
open class App

fun main(args: Array<String>) {
runApplication<App>(*args)
}

You may have noticed the presence of the keyword "open" preceding the classes App, DomainInjectionConfig, and InMemoryOrderRepository. Here is the rationale behind this choice:

note

In Kotlin, classes are final by default, meaning they cannot be subclassed unless explicitly marked as open. This is different from languages like Java, where classes are open for inheritance by default unless marked as final.

In Spring Boot (and Spring Framework in general), many of its features rely on proxy-based mechanisms. These mechanisms involve subclassing beans to apply aspects like transaction management, security, lazy initialization, and other cross-cutting concerns. For these proxy-based features to work, Spring needs to be able to create subclasses of certain beans, which means the classes need to be open.

For the purpose of this exercice we decided to use the open modifier to make our classes annotated with Spring not final as we have few classes, but for large application we can use the All-open compiler plugin instead of preceeding each classe required to be open with the open keyword.

Finally, let's run the application either using your IDE such as Intellij Idea or from command line using maven as follow:

mvn spring-boot:run

Below a screenshot of the place order request response overview:

Screenshot

Conclusion

This post has walked you through the entire process of building a fully functional Microservice, from design to implementation, using DDD and Hexagonal Architecture. My goal was to share knowledge and experiences regarding the methodology, architecture, and patterns needed to create a maintainable, extensible, and deployable Microservice. However, delivering a production-ready product requires addressing more advanced aspects. Below is a non-exhaustive list of such considerations:

  • Api Errors Handling
  • Application security
  • Database persistency
  • Api documentation
  • Env variable config

Keep in mind that no single pattern, architectural style, or programming language suits all software product requirements. It's important to focus on understanding and defining the requirements, parameters, and challenges to make the most informed and effective decisions


References:


Software Architecture: The Implementation View

· 7 min read
Reda Jaifar
Lead Developer

author photo source

The 1+4 Model View The 4+1 view model describes an application’s architecture using four views, along with scenarios that show how the elements within each view collaborate to handle requests

Includes the result of the build process that can be run or deployed such as a Java JAR or Node.js Package. These artifacts interact with each other in the form of a composition or dependency relationship.

The Monolithic Architecture Style

Let's extract the definition of monolithic architecture from an example. Imagine you are invited to develop an enterprise application for managing music concerts ticketing, One of the requirements is to access the system from the browser and a mobile native application. SO the application will handle HTTP requests, execute a function and access a database to persist the data. One of the design options we may have is to create different components each one is responsible for a specific business logic (event subscription, payments, ticket editing ...). if we choose to develop with the java programming language and the spring framework, we'll have one application with many modules interconnected and coupled to accomplish the job. But what about the deployment? what type of build output will generate and how to deploy it into a production environment. The answer is we will generate a single Java WAR file. author

The monolithic representation of our example application (Music Event Application) where we can distinguish bounded functions of the system but all in one artifact

This is what monolithic architecture is about to define the output of your source code as one piece that you can easily:

  • Deploy (push or put into the production environment, or any other environment such as development or staging)
  • Scale (run multiple instances of the application in response to increasing traffic)
  • Debug (in case of non-normal behavior of the system you can explore the logs, check the config, and so on to find the error, all these things are on the same process)
  • Question: Now the system is up and running, but a new feature is required which needs to update the payment provider within our application, how can we achieve that?
  • Answer: we have to update the source code, re-build the whole application, think of a deployment strategy to ensure service continuity of our application.

In the context of our monolithic application, many drawbacks are rising while changing a small piece of the system:

  • Even though the change concern only one part of the system, this one becomes indivisible and decoupled, the build and deploy process is slower because all the source code should be re-build to generate the new artifact (Java WAR file)
  • The whole system is developed with one stack which limits the on-boarding of other developers with different backgrounds
  • Less re-usability of the components.
  • Increasing the artifact (build output) volume.
  • Reliability as one bug in the ticket editing component can cause the whole system to shut down.

In the next section, we discuss the alternative and how microservices address many of the drawbacks of monolithic and bring new added value but also some very challenging points to handle.

The Microservices Architecture Style

Microservices architecture style organizes the application as a set of loosely coupled, independently deployable services, Together these services deliver the functional and business features of the system we want to build. Let's continue with our Music Event Application example and try in the above illustration to define its microservices architecture:

author The Microservices representation of our example application (Music Event Application) where 3 services communicate through HTTP using REST

As we can observe in the illustration each service run in an independent process and also could have its database(recommended), Notice also how these services communicate to each other, in this example, I suggest using the REST API through HTTP, but this is not the only communication option we can have, there are more such as messaging using a message broker.

Let's tackle with further detail the microservices inter-communications in a dedicated article, so far and the rest of this document we will use REST as a reference.

What is a service?

As the word service is a most recurrent when we explore the microservice architecture, Here is a definition:

A service is an independent deployable application or software component that provides a set of functionalities accessible through an API. Service has its own logical architecture, Hexagonal architecture may fit many use-cases, In addition a service can be developed with its specific technology stack that may differ from other services' technology stacks in a microservices architecture

read more about Hexagonal Architecture and alternatives in this article

What is loosely coupled Services and why they should?

Two services are loosely coupled if changes in the design, implementation, or behavior in one won't cause change in others. In a Microservices architecture, the coupling will happen when a change in one enforces an almost immediate change to one or more microservices that collaborate with it directly or indirectly.

While designing Microservices architecture, to make the services the less coupling possible, consider the following points:

Database sharing

the data storage is a microservice implementation detail that should be hidden from its clients (usually other microservices). If Microservice A needs to access data of Microservice B, B should provide an API that A will use to consume the needed data

Code Sharing

By definition, microservices do not share codebase, but we may want to avoid redundancy by sharing dependency libraries and end up needing to update frequently in response to that libraries' client's change requests. So shared code should be as minimum as possible. A good practice that may seem strange at glance is to duplicate code so each service has its copy, so we need to update the library to match Service A requirements, Service B remains un-impacted

Synchronous Communication

In a Microservice architecture, services cooperate to accomplish the job, so they need to communicate either asynchronously or synchronously where the service caller expects a timely response from the callee service might even block while it waits. To address the potential response latency, we can integrate a caching mechanism or implement the circuit breaker pattern to avoid cascading failures. These two options could help remediate the system quickly, but for the long term, the best alternative is switching to asynchronous communication by using a messaging broker like Apache Kafka, So services can cooperate by publishing and consuming messages.

When it comes to designing the next-generation software, relying on a strong and reliable architecture helps a lot, In recent decades, much great software conquered the market and is serving millions of users while scaling up and down to reduce cost and energy or respond to an increasing number of requests. Microservices Architecture is part of other practices and engineering designs behind thanks to its benefits, below is a non-exhaustive list:

  • Independent development: microservices can be developed in isolation to accomplish a defined functionality
  • Independent deployment: microservices can be deployed individually and independently in any environment (cloud, on-premise, managed infrastructure)
  • Fault isolation: if one service fails, the system remains up and only the functionality provided by that stopped microservice will be impacted
  • Technology stack: different programming languages, frameworks, and technologies can be used to build the same software, usually a SaaS
  • Individually scaling: each service can scale as per need, is not necessarily to scale the whole system as is the case of monolithic based application

Despite the number of advantages Microservices Architecture is bringing, choosing it over Monolithic Architecture relies upon on the context, the application domain (banking, delivery, e-commerce, ...) and scope (either is a lightweight application or a complex evolving application), your organization software engineering capabilities and culture.