While older applications tend to rely on monolithic architectures, newer software often uses microservices and cloud-native infrastructures. These decoupled architectures are more technologically efficient and resilient, as well as being easier to develop and maintain. But migrating from monoliths to microservices is not always straightforward. In this article, we’ll be looking at how and why you should upgrade your applications with microservices.
Introduction: why upgrade legacy programs?
Migrating legacy software is not always a top priority, but there are ways to make the process easier.
The problem with legacy systems
Even when well-written, legacy applications often have large and complex codebases, with many internal dependencies. Bugs are hard to fix and functional changes may take much longer than necessary, generating unwanted side effects. This makes applications inflexible, which can be a problem in environments where regulations and administrative constraints change frequently.
Legacy apps also tend to have outdated dependencies – deprecated server software, libraries and platforms, as well hardware. App modernization mitigates problems caused by a lack of support and security vulnerabilities.
Microservices architecture as a key component
Microservices are characterized by loose coupling, fine-grained functionality and lightweight protocols. These facets allow each microservice to be maintained independently, promoting greater functional flexibility and smaller development teams. The service-oriented architecture also opens up the possibility of wider provision for other apps or client uses with concepts like ‘Data as a Service’ (DaaS). Client apps and interoperable systems can make use of limited exposed functionality through APIs. Such agility is key to digital transformation, allowing a flexible combination of data services using composition or aggregator patterns.
Transformation overview
Clearly microservices offer a very different mode of functioning to older, monolithic apps, but migration can be challenging. Strategies must, above all, focus on data and functionality rather than low-level details.
Plan your strategy: establish clear perimeters
When planning app modernization, your primary focus should be on business functionality rather than implementation. Determining microservice domains should make data handling central. Each microservice encapsulates its own logic and data, and only communicates what is required to work with other cohesive microservices.
Microservices design requires deep analysis. A typical design process will involve multiple stakeholders including domain experts and developers working together to form a domain model that determines the perimeters of each microservice’s operations and their lines of communication. This is an iterative and ongoing process. There are no hard and fast rules – ultimately it is a question of judgment.
Microservices patterns for better modernization
Microservices design patterns help you avoid trying to reinvent the wheel. For application modernization, the Strangler Fig pattern is an important refactoring pattern. In nature, the strangler fig is a parasitic plant – one that grows upon another (and eventually kills it). In the case of software, it entails the maintenance of a legacy app while aspects of functionality are incrementally replaced by new microservices – a process governed by Event Interception and Asset Capture, or integrated into already pre-built microservices using new APIs.
Note that the two structures are completely separate in terms of implementation. There is no interruption of service for the end user, but once the complete migration has taken place, the legacy app is no longer needed (cut-off phase).
There are of course several other useful patterns for microservices – for example, the Ambassador pattern, which offloads client connectivity functions like logging, routing and monitoring to a separate service. Also worth considering is the Bulkhead pattern, which abstracts management of crucial infrastructural aspects like memory, CPU and connection pooling to avoid resource hogging. Other useful patterns to know about are the Circuit Breaker pattern and the Gateway Aggregation Pattern.
Patterns like these are a response to challenges that occur frequently in software development, albeit with different technical details. The use of patterns in app modernization prevents you from repeating work. Patterns can also act as building blocks to guide further development of your own microservices architecture.
The role of Domain Driven Design (DDD)
Domain-driven design is a framework for developing application models based on business functionality rather than messaging or horizontal interactivity. It begins with breaking down the functional requirements of a business scenario into discrete domains and subdomains, using the Context Map to analyze and visualize interactions between bounded contexts.
Each component defined in terms of domains encapsulates a specific area of knowledge. Interactions do not require clients to share this knowledge. Services divided by domains foster the ideals of cohesion, encapsulation and loose coupling. Such services are said to have ‘bounded contexts’ – that is, each contains a domain model that is a sub-domain of the larger application, relating to different aspects of the same core entity. Components so divided are easier to maintain. Each can be updated or improved without knock-on effects on the others, provided the bounded contexts are respected.
The result of the DDD analysis directly impacts the size, complexity and performance of the final microservices architecture. Generally choosing aggregates as the basis for microservices is the right solution. In some cases it is possible to directly use the Bounded contexts in the face of a larger dimension. Using DDD entities directly is discouraged.
Tackling complexity with microservices
With complex domains, the most effective microservice architecture may not be initially obvious. This is to be expected, and good results require in-depth iterative analysis with ongoing adaptations as necessary. A particular area of difficulty is with domain interaction – where the same real-world entity is addressed by different domains. This is where bounded contexts really come into play. Though two domains may ostensibly be concerned with the same entity, typically they address different aspects, and it is these aspects – in other words, independent bounded contexts – rather than the thing itself that form the core data.
One particular difficulty is the maintenance of data consistency across distributed transactions. This is a common difficulty for online shopping applications, where inventory management, payment, delivery and other details form an ‘all or nothing’ scenario – if one aspect fails, all must be rolled back. However, where each aspect is contained in a different microservice, some care is needed.
The Saga pattern is one solution. It manages transactions as a sequence of local operations where each must roll back independently. For this, they must be idempotent and retryable. Coordination is governed by a Saga Execution Coordinator, which retains a log of the event sequence.
It’s useful to mention that the motivation for the need for a distributed transaction manager in distributed systems (like microservices) is the CAP Theorem, based on the principles of consistency, availability, and partition tolerance.
In principle, microservices should be as small and independent as possible, providing the greatest internal coherence and the loosest coupling. However, in reality, compromises are usually made in the design phase between architectural purity and implementational complexity.
Logically separate but related services may result in excess ‘chattiness’. This impacts performance by increasing network load. There is therefore a practical limit to how small your microservices should go. A general rule of thumb is to push specificity to the point at which communication boundaries begin to expand quickly. If there is too much communication between two services, they probably are probably not truly autonomous and should be combined.
Ultimately, a microservices architecture should be both scalable and manageable – these are, after all, the main improvements on monolithic applications. To ensure this, the following points provide a useful checklist:
- Single concern. Each microservice should do one thing only.
- Encapsulation. A microservice should not be communicating beyond its deployment unit for core data or logic. Data should be kept and only shared when necessary.
- Container independence. Microservices should not have implementation details that are dependent on their environment, and thus should be easily transportable.
- Ephemerality. Microservices should be able to be created and destroyed on demand. This allows easy scalability and recovery from hardware or network failures.
Ensuring a successful modernization process
Let’s finish up with some further tips gathered from AlmavivA on how to manage your app modernization process.
Combine technical and functional expertise
Designing microservice architectures is not a hard and fast science. Domain experts can advise in depth on what the application should achieve while technical experts understand the kinds of structures that work and can ensure separation of concerns. A technique that AlmavivA have found particularly useful is Event Storming, a workshop-based collaboration strategy for determining domain functionality.
Retain service throughout migration
Downtime is a significant disincentive to app modernization. However, by maintaining continuous service, users need no direct awareness that the underlying structure of their systems is changing. Incremental approaches like the Strangler Fig pattern are invaluable here. Plus you should be ready to roll back changes if they do not work the first time.
Guarantee the evolution of legacy applications
Along with continual service retention, the Strangler Fig pattern helps to guarantee the continued evolution of legacy applications. It’s important to bear in mind that such transitions can take time, but persistence will pay off ultimately.
Benefits of a well-executed app modernization strategy
Particularly with large legacy applications, the application modernization process can be long and involved. Remember though, that a well-executed strategy already employs many of the workflow benefits of a fully refactored microservices-oriented application. The separation of concerns, manageable architecture, simpler code base and more functional teamwork should be enough motivation!