Enterprise Microservices Design [Part 2: Inner Architecture Zone]
The generally accepted practice when building your MSA is to focus on how you would scope out a service that provides a single-function rather than the size. The inner architecture typically addresses the implementation of the microservices themselves. Significantly, the inner architecture needs to be simple, so it can be easily and independently deployable and independently disposable.
A good microservice design will ensure that six factors have been considered when scoping out and designing the inner architecture:
- The microservice should have a single purpose and single responsibility, and the service itself should be delivered as a self-contained unit of deployment that can create multiple instances at the runtime for scale.
- The microservice should have the ability to adopt an architecture that’s best suited for the capabilities it delivers and one that uses the appropriate technology.
- Once the monolithic services are broken down into microservices, each microservice or set of microservices should have the ability to be exposed as APIs. However, within the internal implementation, the service could adopt any suitable technology to deliver that respective business capability by implementing the business requirement. To do this, the enterprise may want to consider something like Swagger to define the API specification or API definition of a particular microservice, and the microservice can use this as the point of interaction. This is referred to as an API-first approach in microservice development.
- With units of deployment, there may be options, such as self-contained deployable artifacts bundled in hypervisor-based images, or container images, which are generally the more popular option.
- The enterprise needs to leverage analytics to refine the microservice, as well as to provision for recovery in the event the service fails. To this end, the enterprise can incorporate the use of metrics and monitoring to support this evolutionary aspect of the microservice.
- Even though the microservice paradigm itself enables the enterprise to have multiple or polyglot implementations for its microservices, the use of best practices and standards is essential for maintaining consistency and ensuring that the solution follows common enterprise architecture principles. This is not to say that polyglot opportunities should not be completely vetoed; rather they need to be governed when used.
2.1 Identifying Microservices
One of the biggest challenges of microservices is to define the boundaries of individual services. The general rule is that a service should do “one thing” — but putting that rule into practice requires careful thought. There is no mechanical process that will produce the “right” design. You have to think deeply about your business domain, requirements, and goals. Otherwise, you can end up with a haphazard design that exhibits some undesirable characteristics, such as hidden dependencies between services, tight coupling, or poorly designed interfaces.
Microservices should be designed around business capabilities, not horizontal layers such as data access or messaging. In addition, they should have loose coupling and high functional cohesion.
Domain-driven design (DDD) provides a framework that can get you most of the way to a set of well-designed microservices. DDD has two distinct phases, strategic and tactical. In strategic DDD, you are defining the large-scale structure of the system. Strategic DDD helps to ensure that your architecture remains focused on business capabilities. Tactical DDD provides a set of design patterns that you can use to create the domain model. These patterns include entities, aggregates, and domain services. These tactical patterns will help you to design microservices that are both loosely coupled and cohesive.
General Guidelines to Identify Microservices
- Start by analyzing the business domain to understand the application’s functional requirements. The output of this step is an informal description of the domain, which can be refined into a more formal set of domain models.
- Next, define the bounded contexts of the domain. Each bounded context contains a domain model that represents a particular subdomain of the larger application.
- Within a bounded context, apply tactical DDD patterns to define entities, aggregates, and domain services.
- Use the results from the previous step to identify the microservices in your application.
2.2 Microservices Implementation
Once a microservice has been identified we follow a bottoms-up approach for implementation. Below are the 6 step bottom-up approach that is followed during the development process
- Identifying Storage Requirements
- Identify Microservices communication
- API Design
- Class Design
2.2.1 Identifying Storage
A basic principle of microservices is that each service manages its own data. Two services should not share a data store. Instead, each service is responsible for its own private data store, which other services cannot access directly.
The reason for this rule is to avoid unintentional coupling between services, which can result if services share the same underlying data schemas. If there is a change to the data schema, the change must be coordinated across every service that relies on that database. By isolating each service’s data store, we can limit the scope of change, and preserve the agility of truly independent deployments. Another reason is that each microservice may have its own data models, queries, or read/write patterns. Using shared datastore limits each team’s ability to optimize data storage for their particular service.
General Guidelines to Managing Data
- Identify the type of Data (relational, non-relational, object) and use the appropriate storage mechanism
- Embrace eventual consistency wherever possible.
- When you need strong consistency guarantees, one service may represent the source of truth for a given entity, which is exposed through an API.
- For transactions, use patterns such as Scheduler Agent Supervisor and Compensating Transaction to keep data consistent across several services
- Store only the data that a service needs. A service might only need a subset of information about a domain entity.
- If two services are continually exchanging information with each other, resulting in chatty APIs, you may need to redraw your service boundaries, by merging two services or refactoring their functionality.
2.2.2 Identifying Microservices Communication
In Microservices architecture, most of the time every service depends/consume other services running somewhere else, either it can be in-house or cloud. This communication can be of two types
- East-West: In this type of communication, microservices communicate with other microservices within the Inner Architecture zone of the enterprise
- North-South: In this type of communication, microservices communicate with External Architecture zone services
General Guidelines for Microservices communication
- Identify all the services the microservices needs to communicate
- Identify and document the communication protocol
- Identify and document the API request and response structure
- Create a sequence diagram of the necessary behavior
Many times our microservices depend on other services to proceed with our development. Due to the agile nature of development, some of the services may not be available during the development of our microservices and we need a way to mock these services to proceed further with development. Although there are various tools available for mocking, we will consider Wiremock for this article
WireMock is a library for stubbing and mocking web services. It constructs an HTTP server that we could connect to as we would to an actual web service.
When a WireMock server is in action, we can set up expectations, call the service, and then verify its behaviors. Our microservices can then interact with this mock server as if it’s interacting with the actual server.
2.2.3 API Design
Good API design is important in a microservices architecture because all data exchange between services happens either through messages or API calls. APIs must be efficient to avoid creating chatty I/O. Because services are designed by teams working independently, APIs must have well-defined semantics and versioning schemes, so that updates don’t break other services.
General Guidelines for API Design
- REST APIs should accept JSON for request payload and also send responses to JSON. JSON is the standard for transferring data.
- We shouldn’t use verbs in our endpoint paths. Instead, we should use the nouns which represent the entity that the endpoint that we’re retrieving or manipulating as the pathname.
- The action should be indicated by the HTTP request method that we’re making. The most common methods include GET, POST, PUT, and DELETE. GET retrieves resources. POST submits new data to the server. PUT updates existing data. DELETE removes data. The verbs map to the CRUD operations.
- To eliminate confusion for API users when an error occurs, we should handle errors gracefully and return HTTP response codes that indicate what kind of error occurred.
- Most communication between client and server should be secure since we often send and receive private information.
- We should have different versions of API if we’re making any changes to them that may break clients.
When exposing our service to external clients, API documentation becomes the key communication mechanism. API documentation is a technical content deliverable, containing instructions about how to effectively use and integrate with an API. It’s a concise reference manual containing all the information required to work with the API, with details about the functions, classes, return types, arguments and more, supported by tutorials and examples. API Documentation has traditionally been done using regular content creation and maintenance tools and text editors.
API design and documentation is the first step while developing microservices and for BAO the APIs are documented at the below location using Swagger
2.2.4 Class Design
Good design of individual classes is crucial to good overall system design. A well-designed class is more re-usable in different contexts, and more modifiable for future versions of the software. Classes are grouped into the package and as a first step, we identify the required packages in microservices. In general, we recommend having the following packages in your microservices at a minimum
All microservices in Metro follow the below project structure for implementation which promotes cohesiveness and loose coupling
Following are the strategies that feature in testing microservice by developers:
- Unit testing
- API testing
- End to end testing
A unit test is quite voluminous and is internal to the microservice. Ideally, this type of testing should be an automated process and depends upon the development language and the framework within the service.
We recommend use Junit and Mockito to write unit test cases and target for 80% coverage
API testing is a type of software testing that involves testing application programming interfaces (APIs) directly and as part of integration testing to determine if they meet expectations for functionality, reliability, performance, and security.
We recommend use Karate framework to write API test cases for our microservices endpoints
End to End testing:
End to end testing (E2E testing) refers to a software testing method that involves testing an application’s workflow from beginning to end. This method basically aims to replicate real user scenarios so that the system can be validated for integration and data integrity
We recommend use postman to create collections that can test our microservices end to end along with integration services
All Microservices projects should follow the following convention for the version
<Major Version>.<Minor Version>.<Build Version>
Example: 2.0.1 (Major Version = 2, Minor Version = 0 , Minor Version =1)
major is incremented when there is a major revision update of the microservices endpoints that need corresponding updates at consuming services
Example: if we are going live with a new version of our API
minor is incremented when the microservices changes need significant changes at the services consuming the changes.
Example: if we update the Request domain object in the microservice and added fields that are mandatory and needs to be sent by the consuming service
build is incremented when microservices changes don't impact the services consuming the changes
Example: if we update the Request domain object in the microservice and added fields that are non-mandatory and the consuming service need not send them
2.2.8 Best Practices
1) Identify the Microservices based on the business needs and not on the technology (DB, MQ) they work on
2) Each microservice needs its own Database, it is against microservices principles to share a database among multiple microservices
3) Always maintain a list of microservices dependencies
4) Follow the resource naming guide while naming the Rest API endpoints
5) Facade layer classes should not hold any business logic. Below are the only responsibility of this layer
- Receive a request object from the service layer
- Convert the request object from the service layer to Client Request Object
- Send the client request object to the external client and receive the client response object
- Convert the client response object to facade response object
- send the facade response object to service layer
6) To promote loose couple the facade layer should never send the object received as a response from the client to the service layer. In turn, convert the client object to a domain object specific to facade layer response in the microservice and send it to the service layer
7) Use Lombok for Domain Objects
8) Log only when necessary and at appropriate log levels