For organizations working with microservices, success can be varied and gaining the benefits of the cloud can be a costly journey. This blog post will share how to succeed with microservices through microservices principles, domain driven design concepts, and considerations around coding best practices. Our Cloud-Native applications, Kubernetes instances, and microservices all represent a system that’s composed of layers. Understanding these layers allows us to have the insights necessary to unlock cloud and container-native benefits.
The Nature of Systems Design
Systems design is a game of trade-offs. Many architectural decisions are not inherently right or wrong when taken out of organizational context. The best recommendation for making decisions is to scope and frame that decision as best as possible to understand the decision at the time of inception. A fundamental guideline is always to tie these decisions back to the organization’s goals. In the context of an organization, principles, practices, and patterns need to align with the organization’s goals. Principles set the direction towards the goals. Practices and patterns represent the actual steps taken by teams to meet those goals.
For example, my organization could have the goal of becoming a de facto software solution for a global market. One of my principles could be to practice continuous delivery to ensure quality production deployments and to minimize incidents that could be costly. Practices are granular and specific to teams. To support that principle which my engineering business unit follows, I could have an SRE team have practices around incident management that involves using my Continuous Delivery platform to track or audit failed deployments. I could have developers practice frequent releases or self-service deployments using my CD solution. Another practice for my development team is to test all code.
It’s impossible to know how every decision will affect an entire system in the future. The best you can do is to think of your goals and how your principles and practices can help you reach those goals.
Microservices are small, autonomous services that work together. Loosely coupled and high cohesion are the two concepts that refer to microservices. Cohesion is how we group related code together, and coupling refers to how different services depend on each other. Robert C. Martin’s definition of the Single Responsibility Principle, which is at the heart of microservices, states it’s to “gather together those things that change for the same reason, and separate those things that change for different reasons.”
These two concepts drive the seven principles of microservices, which allow teams to work, deploy, fail, deliver, and scale independently.
Service-oriented architectures (SOA) aimed to combat the challenges of large monolithic applications, reusability of code, and maintenance. Microservices is an approach to SOA through independent services, where each service acts as a boundary on our business domain. In microservice architectures, every change can be implemented independently of each other and deployed without requiring consumers to change.
The common point of failure when working with microservices is premature decomposition. Often this is where teams experience a high cost for changes related to an application’s use case or where initial service boundaries were wrong. Decomposing an application into microservices is often the easiest method for starting a microservices journey.
The Principles of Domain Driven Design
Domain-driven design is how to model the real world through code. And so DDD lies in between great code and microservice success. While there exist many pieces of literature that discuss how to implement DDD, both strategically and tactically, this remains a rather complex topic to approach without some practice and guidance. Here’s how to get started on leveraging DDD concepts.
First, it’s essential to understand that any code we work with begins with a problem that lives within a domain and the presence of a business wish. And so, the journey of domain-driven design starts with a domain expert and a developer. Often you may have multiple domain experts and one developer or various developers but only one domain expert. No matter your organizational breakdown, the goal of the team is to focus on the big picture and create what’s called a context map.
When building a context map, you distill domain knowledge by understanding the problem space, discovering ubiquitous language, and creating a representation model for your system. Your system consists of domains and subdomains that represent your problem space. These domains are called contexts within a context map and may describe different systems within an organization. For example, I may need to represent a sales context and customer support context to model a new software application that handles the sales and customer support of a food packaging factory.
These domains give you a good idea about how to create bounded contexts. Bounded contexts represent services belonging to our system, and it encapsulates and defines the specific responsibilities of that model. Creating a bounded context is about establishing boundaries in which domain language exists without confusion within that space.
Defining bounded contexts, ubiquitous language, and context maps allow you to focus on the big picture when working with microservices. Domain-driven design guides developers when discussing system designs as we’re often looking for ways to represent the real world through code. DDD can be especially useful for organizations or developers who are new to specific domains or for organizations looking to decompose their applications into microservices pain-free.
The final piece to succeeding with microservices is on how we maintain and work with our code. There are many suggestions for encouraging long-lasting and understandable enterprise codebases. Some of them introduce additional trade-offs, but the general rule of thumb is to avoid complacency with growing codebases and to find where the following practices could be useful for your organization.
Provide shared libraries. Approaches that repeat across domains, industries, teams, and various codebases are great candidates for shared libraries. Third-party or custom libraries are a great way to keep code bases well managed and tested, especially as organizations continue to develop further features and services within a domain. I recommend holding off on introducing custom libraries for code that is subject to frequent change. Custom libraries add application dependencies where updates to the library force consumers to redeploy. Trusted or mature third-party libraries are often a great resource to avoid some of the maintenance and instability associated with custom libraries.
Enforce Modular Separation. As often as we hear about modular separation as a recommendation, it often fails in practice because of the nature of change. As new features, developers and processes are introduced to codebases, how we structure the modules and files that provide these features also changes. Keeping each appropriately sized also becomes very important. As a guideline, set a few practices as a team to how you would like to organize business logic within your codebase. Some teams have three tiers of organization, which include the presentation tier, the logic tier, and the data tier. This strategy ensures that business logic does not get lost within application logic. Enforcing modular separation of code helps teams succeed with DDD as well.
Keeping a Codebase Small. Many of the prior suggestions lead to maintaining codebases smaller. However, a common question that often arises around keeping codebases lean and small is how small is too small? In many ways, small codebases become an anti-pattern as teams fail to understand their service provides business responsibilities in the context of an entire system. Likewise, for large codebases, teams will struggle to decentralize decisions, understand their codebase, and handle other forms of change. A key indicator for both these challenges is an uptick in questions.
Maintaining a clean codebase is integral to DDD, microservices, and writing Kubernetes or cloud-native applications. Just as Kubernetes, Microservices, and DDD influence how we design our code. The hope is that these explanations illustrate how our applications are composed of layers that overlap and complement each other to form a working and successful system.
Many organizations investing into Kubernetes initiatives are looking to succeed with microservices. This blog post peels back the layers of the onion to show how to succeed with microservices.