The delicate process of extracting a microservice from a monolith application can be more akin to brain surgery than feature development: It’s hard to be certain where complications lie until you dive in. Monolith applications are often the result of generations of architectures, patterns, and approaches. To create a microservice you have to first identify the bounded context—i.e., the boundary that separates one service from the rest of the system. Even then, you may find that ownership of the code which supports it spans multiple teams and is beyond the point of any one person’s comprehension. Meanwhile, the business processes within a bounded context are usually dependent on other processes outside of it. Extracting the core code of the bounded context and resolving the dependencies between different contexts makes up the bulk of work required to tease a monolith system apart. However, the discoveries that come out of analyzing these dependencies and the subsequent decisions about how to resolve them are going to help build the backlog of work for a microservice effort. This can be a bit of a surgical process, but if done strategically, you’ll end up with a more robust and autonomous system.
Identifying the core code
The first step to extracting a microservice is identifying the core code of the bounded context. A logical place to start is looking the UI components or views and API endpoints that support the business processes of the bounded context, and tracing the execution path of each operation. Often, the logic is contained within multiple services, domain objects, and request handlers, which may or may not be contained within isolated namespaces or assemblies within the monolith. Let’s say we’re trying to extract the billing service from a healthcare application that tracks patient records, appointment scheduling, and billing, for example. The majority of the billing logic may be concentrated in one or a few top-level namespaces.
Once you’ve identified the core code of the system, it is necessary to evaluate dependencies that live outside of the bounded context. These have to be resolved so you can isolate and extract the microservice. Typically, we classify dependencies into two categories: internal and external. Internal dependencies refer to logic outside of a bounded context that is necessary to fulfill operations within. In our healthcare example, the billing module might depend on system-wide logging infrastructure or a notification feature that is tangential to the operations of scheduling. External dependencies refer to code outside of a bounded context that depends on logic within the context to fulfill its operations. The healthcare system in our example may have functionality that depends on the billing system when a doctor orders a test for a patient.
NDepend is a great tool for identifying and analyzing dependencies within an application. It provides a rich interface for querying information about a code base. Creating visualizations can also help you to conceptualize the target service and figure out the work necessary to extract it from a monolith. NDepend provides some visualization functionality, but manually creating high-level diagrams using tools like Draw.io or Lucidchart can give you greater control. The example below is a manually-drawn map of the dependencies in the healthcare app we talked about:
Each internal and external dependency represents one or more use case that has to be supported once the microservice is extracted from the monolith. These dependencies can be resolved in a number of ways, but will likely require refactoring. When determining the best approach for resolving them, it’s helpful to classify dependencies as supporting either synchronous or asynchronous operations. A synchronous dependency is one in which a caller needs confirmation that an operation has been completed before completing its own process. For example, if the patient record module of our healthcare system is required to log changes for compliance purposes, the notes feature (caller) may need confirmation that a log entry was successful before completing the process of adding a note. Creating a shared library or an API are possible solutions to resolving synchronous dependencies. An asynchronous dependency does not require that an operation complete before completing its own process. For example, if the billing module needs to be updated when a test is ordered for a patient, a doctor shouldn’t be prevented from ordering the test if the billing module is temporarily inaccessible. In this case, it may make sense to introduce messaging with a tool like NServicebus, to allow the patient record module to notify the billing service when a test has been ordered.
Determining data ownership
Dependencies between a bounded context and the larger system will likely surface within the data model, as well as the code. Because of this, it’s important to evaluate data ownership within a system. Examining read and write access to data is a helpful approach to determining what data should be owned by a particular microservice. When resolving dependencies within the data model, it may be necessary to introduce new entities and tables to enable autonomous services. Messaging and APIs can also be employed to access dependent data from other services. While analyzing the dependencies of a bounded context, it can be helpful to document use cases and options for resolving their dependencies. This can be useful not only for tracking discoveries but for regression testing as well, especially in systems that don’t currently have sufficient unit and integration test coverage. Extracting a microservice from a monolith is a delicate process that may not be feasible to do in a big-bang approach, especially for larger services. Oftentimes, there is a desire to implement the ideal solution, instead of working piecemeal. It’s important to remember that breaking down a monolith is a strategic process that will take time to fully realize. A phased approach to refactoring and resolving dependencies within a system will allow for short-term gains to be realized. This, along with a systematic approach to identifying, analyzing, and resolving dependencies, will create the clarity and help streamline the process of breaking apart a monolith.
For more information on resolving dependencies and integrating microservices, take a look at Principal Consultant, Yogi Aradhye’s post, Principles For Microservice Integration, and our Chief Architect Jimmy Bogard’s series on Refactoring Towards Resiliency.