Book Image

TypeScript Microservices

Book Image

TypeScript Microservices

Overview of this book

In the last few years or so, microservices have achieved the rock star status and right now are one of the most tangible solutions in enterprises to make quick, effective, and scalable applications. The apparent rise of Typescript and long evolution from ES5 to ES6 has seen lots of big companies move to ES6 stack. If you want to learn how to leverage the power of microservices to build robust architecture using reactive programming and Typescript in Node.js, then this book is for you. Typescript Microservices is an end-to-end guide that shows you the implementation of microservices from scratch; right from starting the project to hardening and securing your services. We will begin with a brief introduction to microservices before learning to break your monolith applications into microservices. From here, you will learn reactive programming patterns and how to build APIs for microservices. The next set of topics will take you through the microservice architecture with TypeScript and communication between services. Further, you will learn to test and deploy your TypeScript microservices using the latest tools and implement continuous integration. Finally, you will learn to secure and harden your microservice. By the end of the book, you will be able to build production-ready, scalable, and maintainable microservices using Node.js and Typescript.
Table of Contents (17 chapters)
Title Page
Copyright and Credits
Packt Upsell
Contributors
Preface
Index

Microservice design patterns


As microservices evolve, so evolves its designing principles. Here are some of the common design patterns that help to design an efficient and scalable system. Some of the patterns are followed by Facebook, Netflix, Twitter, LinkedIn, and so on, which provide some of the most scalable architectures.

Asynchronous messaging microservice design pattern

One of the most important things to consider in a distributed system is state. Although highly powerful REST APIs, it has a very primitive flaw of being synchronous and thus blocking. This pattern is about achieving a non-blocking state and asynchronicity to maintain the same state across the whole application reliably, avoid data corruption, and allow a faster rate of change across the application:

  • Problem: Speaking contextually, if we go with the principle of single responsibility, a model or an entity in the application can mean something different to different microservices. So, whenever any change occurs, we need to ensure that different models are in sync with those changes. This pattern helps to solve this issue with the help of asynchronous messaging. In order to ensure data integrity throughout, there is a need to replicate the state of key business data and business events between microservices or datastores.
  • Solution: Since it's asynchronous communication, the client or the caller assumes that the message won't be received immediately, carries on and attaches a callback to the service. The callback is for when the response is received what further operation to be carried on. A lightweight message broker (not to be confused with orchestrators used in SOA) is preferably used. The message broker is dumb, that is, they are ignorant of the application state. They communicate to services handling events, but they never handle events. Some of the widely adopted examples include RabbitMQ, the Azure bus, and so on. Instagram's feed is powered by this simple RabbitMQ. Based on the complexity of the project, you can introduce either a single receiver or multiple receivers. While a single receiver is good, soon it can be the single point of failure. A better approach is going reactive and introducing the publish-subscribe pattern of communication. That way the communication from the sender will be available to subscriber microservices in one go. Practically, when we consider a routine scenario, an update in any of the models will trigger an event to all its subscribers, which may further trigger the change in their own models. To avoid this, event bus is generally introduced in such type of a pattern that can fulfill the role of inter micro service communication and act as the message broker. Some of the commonly available libraries are AMQP, RabbitMQ, NserviceBus, MassTransit, and so on for scalable architecture.
  • Take care of: To successfully implement this design, the following aspects should be considered:
  • When you need high scalability, or your current domain is already a message-based domain, then preference should be given to message-based commands over HTTP.
  • Publishing events across microservices, as well as changing the state in the original microservices.
  • Make sure that events are communicated across; mimicking the event would be a very bad design pattern.
  • Maintain the position of the subscriber's consumer to scale up performance.
  • When to make a rest call and when to use a messaging call. As HTTP is a synchronous call, it should be used only when needed.
  • When to use: This is one of the most commonly used patterns. Based on the following use cases, you can use this pattern or its variants as per your requirements:
  • When you want to use real-time streaming, use the Event Firehouse pattern, which has KAFKA as one of its key components.
  • When your complex system is orchestrated in various services, one of the variants of this system, RabbitMQ, is extremely helpful.
  • Often, instead of subscribing to services, directly subscribing to the datastore is advantageous. In such a case use, GemFire or Apache GeoCode following this pattern is helpful.
  • When not to use: In the following scenarios, this pattern is less recommended:
  • When you have heavy database operations during event transmission, as database calls are synchronous
  • When your services are coupled
  • When you don't have standard ways defined to handle data conflict situations

Backend for frontends

The current world demands a mobile-first approach everywhere. The service may respond differently to mobile where it has to show little content, as it has very less content. On the web, it has to show huge content as lots of space is available. Scenarios may differ drastically based on the device. As for example in the mobile app, we may allow barcode scanner, but in desktop, it is not a wise option. This pattern addresses these issues and helps to effectively design microservices across multiple interfaces:

  • Problem: With the advent of development of services supporting multiple interfaces, it becomes extremely painful to manage everything in one service. This constantly evolves change in any of the single interfaces; the need to keep services working in all interfaces can soon become a bottleneck and a pain to maintain.

  • Solution: Rather than maintaining a general purpose API, design one backend per user experience or interface, better termed as a backend for frontend (BFFs). The BFF is tightly bound to a single interface or specific user experience and is maintained by their specific teams so as to easily adapt to new change. While implementing this pattern, one of  the common concerns that occurs is maintaining the number of BFFs. A more generic solution would be separating concerns and having each BFF handle its own responsibility.

  • Take care of: While implementing this design pattern, the following points should be taken care of as they are the most common pitfalls:

  • A fair consideration of the amount of BFFs to be maintained. A new BFF should only be created when concerns across a generally available service can be separated out for a specific interface.
  • A BFF should only contain client/interface-specific code to avoid code duplication.
  • Divide responsibilities across teams for maintaining BFFs.
  • This should not be confused with a Shim, a converter to the convert to interface-specific format required for that type of interface.
  • When to use: This pattern is extremely useful in the following scenarios:

  • There are varying differences in a general-purpose backend service across multiple interfaces and there are multiple updates at any point in time in a single interface.
  • You want to optimize a single interface and not disturb the utility across other interfaces.
  • There are various teams, and implement an alternative language for a specific interface and you want to maintain it separately.
  • When not to use: While this pattern does solve lots of issues, this pattern is not recommended in the following scenarios:

  • Do not use this pattern to handle generic parameter concerns such as authentication, security, or authorization. This would just increase latency.
  • If the cost of deploying an extra service is too high.
  • When interfaces make the same requests and there is not much difference between them.
  • When there is only one interface and support for multiple interfaces is not there, a BFF won't make much sense.

Gateway aggregation and offloading

Dump or move specialized, common services and functionalities to a gateway. This pattern can introduce simplicity by moving shared functionality into a single part. Shared functionality can include things such as the use of SSL certificates, authentication, and authorization. A gateway can further be used to join multiple requests into a single request. This pattern simplifies needs where a client has to make multiple calls to different microservices for some operation:

  • Problem: Often, to perform a simple task, a client may need to make multiple HTTP calls to various different microservices. Too many calls to a server requires an increase in resources, memory, and threads, which adversely affects performance and scalability. Many features are commonly used across multiple services; an authentication service and a product checkout service are both going to use the log in the same way. This service requires configuration and maintenance. Also, these type of services need an extra set of eyes as they are essential. For example, token validation, HTTPS certificate, encryption, authorization, and authentication. With each deployment, it is difficult to manage that as it has to span across the whole system.

  • Solution: The two major components in this design pattern are the gateway and gateway aggregator. The gateway aggregator should always be placed behind the gateway. Hence, single responsibility is achieved, with each component doing the operation they are meant to do.

  • Gateway: It offloads some of the common operations such as certificate management, authentication, SSL termination, cache, protocol translation, and so on to one single place. It simplifies the development and abstracts all this logic in one place and speeds up development in a huge organization where not everyone has access to the gateway, only specialized teams work on it. It maintains consistency throughout the application. The gateway can ensure a minimum amount of logging and thus help out to find the faulty microservice. It's much like the facade pattern in object-oriented programming. It acts as the following:

  • Filter
  • Single entry point that exposes various microservices
  • Solution to a common operation such as authorization, authentication, central configuration, and so on, abstracting this logic into a single place
  • Router for traffic management and monitoring

Netflix uses a similar approach and they are able to handle more than 50,000 requests per hour and they open sourced ZuuL:

  • Gateway aggregator: It receives the client request, then it decides to which different systems it has to dispatch the client request, gets the results, and then aggregates and sends them back to the client. For the client, it is just one request. Overall round trips between client and server are reduced.
  • Take care of: The following pitfalls should be properly handled in order to successfully implement this design pattern in microservices:
  • Do not introduce service coupling, that is, the gateway can exist independently, without other service consumers or service implementers.
  • Here, every microservice will be dependent on the gateway. Hence, the network latency should be as low as possible.
  • Make sure to have multiple instances of the gateway, as only a single instance of the gateway may introduce it as a single point of failure.
  • Each of the requests goes through the gateway. Hence, it should be ensured that gateway has efficient memory and adequate performance, and can be easily scaled to handle the load. Have one round of load testing to make sure that it is able to handle bulk load.
  • Introduce other design patterns such as bulkheads, retry, throttle, and timeout for efficient design.
  • The gateway should handle logic such as the number of retries, waiting for service until.
  • The cache layer should be handled, which can improve performance.
  • The gateway aggregator should be behind the gateway, as the request aggregator will have another. Combining them in a gateway will likely impact the gateway and its functionalities.
  • While using the asynchronous approach, you will find yourself smacked by too many promises of callback hell. Go with the reactive approach, a more declarative style. Reactive programming is prevalent from Java to Node.js to Android. You can check out this link for reactive extensions across different links: https://github.com/reactivex.
  • Business logic should not be there in the gateway.
  • When to use: This pattern should be used in the following scenarios:
  • There are multiple microservices across and a client needs to communicate with multiple microservices.
  • Want to reduce the frequent network calls when the client is in lesser range network or cellular network. Breaking it in one request is efficient as then the frontend or the gateway will only have to cache one request.
  • When you want to encapsulate the internal structure or introduce an abstract layer to a large team present in your organization.
  • When not to use: The following scenarios are when this pattern won't be a good fit:
  • When you just want to reduce the network calls. You cannot introduce a whole level of complexity for just that need.
  • The latency by the gateway is too much.
  • You don't have asynchronous options in the gateway. Your system makes too many synchronous calls for operations in the gateway. That would result in a blocking system.
  • Your application can't get rid of coupled services.

Proxy routing and throttling

When you have multiple microservices that you want to expose across a single endpoint and that single endpoint routes to service as per need. This application is helpful when you need to handle imminent transient failures and have a retry loop on a failed operation, thus improve the stability of the application. This pattern is also helpful when you want to handle the consumption of resources used by a microservice.

This pattern is used to meet the agreed SLAs and handle loads on resources and resource allocation consumption even when an increase in demand places loads on resources:

  • Problem: When a client has to consume a multitude of microservices, challenges soon turn up such as client managing each endpoint and setting up separate endpoints. If you refactor any part of the code in any service then the client must also be updated as the client is directly in contact with the endpoint. Further, as these services are in the cloud, they have to be fault tolerant. Faults include temporary loss of connectivity or unavailability of services. These faults should be self-correcting. For example, a database service that is taking a large number of concurrent requests should throttle further requests until the memory load and resource utilization has decreased. On retrying the request, the operation is completed. The load on any application varies drastically on time period. For example, a social media chatting platform will have very less load during peak office hours and a shopping portal will have extreme load during festive season sales. For a system to perform efficiently it has to meet to agreed LSA, once it exceeds, subsequent requests needs to be stopped until load consumption has decreased.

  • Solution: Place gateway layer in front of microservices. This layer includes the throttle component, as well as retry, once failed component. With the addition of this layer, the client needs only to interact with this gateway rather than interacting with each different microservice. It lets you abstract backend calls from the client and thus keeping the client end simple as the client only has to interact with the gateway. Any number of services can be added, without changing the client at any point in time. This pattern can also be used to handle versioning effectively. A new version of the microservice can be deployed parallelly and the gateway can route too, based on input parameters passed. New changes can be easily maintained by just a configuration change at the gateway level. This pattern can be used as an alternative strategy to auto-scaling. This layer should allow network requests only up to a certain limit and then throttle the request and retry once the resources have been released. This will help the system to maintain SLAs. The following points should be considered while implementing the throttle component:

  • One of the parameters to consider for throttling is user requests or tenant requests. Assuming that a specific tenant or user triggers throttle, then it can be safely assumed that there's some issue with the caller.
  • Throttling doesn't essentially mean to stop the requests. Lower quality resources if available can be given, for example, a mobile-friendly site, a lower quality video, and so on. Google does the same.
  • Maintaining priority over microservices. Based on the priority they can be placed in the retry queue. As an ideal solution, three queues can be maintained—cancel, retry, and retry-after sometime.
  • Take care of: Given here are some of the most common pitfalls that we can come across while successfully implementing this pattern:
  • The gateway can be a single point of failure. Proper steps have to be taken to ensure that it has fault tolerant capabilities during development. Also, it should be run in multiple instances.
  • Gateway should have proper memory and resource allocation otherwise it will introduce a bottleneck. Proper load testing should be done to ensure that failures are not cascaded.
  • Routing can be done based on IP, header, port, URL, request parameter, and so on.
  • The retry policy should be crafted very carefully based on the business requirements. It's okay in some places to have a please try again rather than having waiting periods and retrials. The retry policy may also affect the responsiveness of the application.
  • For effective application, this pattern should be combined with Circuit Breaker Application.
  • If service is idempotent, then and only then should it be retried. Trying retrial on other services may have unhealthy consequences. For example, if there is a payment service that waits for responses from other payment gateways, the retry component may think it fails and may send another request and the customer gets charged twice.
  • Different exceptions should handle the retry logic accordingly, based on the exceptions.
  • Retry logic should not disturb transaction management. The retry policy should be used accordingly.
  • All failures that trigger a retry should be logged and handled properly for future scenarios.
  • An important point to be considered is this is no replacement for exception handling. The first priority should be given to exceptions always, as they would not introduce an extra layer and add latency.
  • Throttling should be added early in the system as it's difficult to add once the system is implemented; it should be carefully designed.
  • Throttling should be performed quickly. It should be smart enough to detect an increase in activity and react accordingly by taking appropriate measures.
  • Consideration between throttling and auto-scaling should be decided based on business requirements.
  • The requests that are throttled should be effectively placed in a queue based on priority.
  • When to use: This pattern is very handy in the following scenarios:
  • To ensure that agreed LSAs are maintained.
  • To avoid a single microservice consuming the majority of the pool of resources and avoid resource depletion by a single microservice.
  • To handle sudden bursts in consumption of microservices.
  • To handle transient and short-lived faults.
  • When not to use: In the following scenarios, this pattern should not be used:
  • Throttling shouldn't be used as a means to handle exceptions.
  • When faults are long-lasting. If this pattern is applied in that case, it will severely affect the performance and responsiveness of the application.

Ambassador and sidecar pattern

This pattern is used when we want to segregate common connectivity features such as monitoring, logging, routing, security, authentication, authorization, and more. It creates helper services that act as ambassadors and sidecars that do the objective of sending requests on behalf of a service. It is just another proxy that is located outside of the process. Specialized teams can work on it and let other people not worry about it so as to provide encapsulation and isolation. It also allows the application to be composed of multiple frameworks and technologies.

The sidecar component in this pattern acts just like a sidecar attached to a motorcycle. It has the same life cycle as the parent microservice, retires the same time a parent microservice does, and it does essential peripheral tasks:

  • Solution: Find a set of operations that are common throughout different microservices and place them inside their own container or process, thus providing the same interface for these common operations to all frameworks and platforms services in the whole system. Add an ambassador layer that acts as a proxy between applications and microservices. This ambassador can monitor performance metrics such as the amount of latency, the resource usage, and so on. Anything inside the ambassador can be maintained independently of the application. An ambassador can be deployed as a container, common process, daemon, or windows service. An ambassador and sidecar are not part of the microservice, but rather are connected to the microservice. Some of the common advantages of this pattern are as follows:
  • Language-independent development of the sidecar and ambassador, that is, you don't have to build a sidecar and ambassador for every language you have in your architecture.
  • Just part of the host, so it can access the same resources as any other microservice can.
  • Due to connection with microservices, there hardly is any latency Netflix uses a similar approach and they have open sourced their tool Prana (https://github.com/Netflix/Prana). Take a look at the following diagram:

  • Take care of: The following points should be taken care of as they are the most common pitfalls:
  • The ambassador can introduce some latency. Deep thought should be given on whether to use a proxy or expose common functionalities as the library.
  • Adding generalized functionalities in ambassador and sidecar is beneficial, but is it required for all scenarios? For example, consider the number of retries to a service, it might not be common for all use cases.
  • The language or framework in which ambassador and sidecar will be built, managed, and deployed strategy for it. The decision to create single or multiple instances of it based on need.
  • Flexibility to pass some parameters from service to ambassador and proxy and vice versa.
  • The deployment pattern: this is well suited when the ambassador and sidecar are deployed in containers.
  • The inter-micro service communication pattern should be such that it is framework agnostic or language agnostic. This would be beneficial in the long run.
  • When to use: This pattern is extremely helpful in the following scenarios:
  • When there are multiple frameworks and languages involved and you need a common set of general features such as client connectivity, logging, and so on throughout the application. An ambassador and sidecar can be consumed by any service across the application.
  • Services are owned by different teams or different organizations.
  • You need independent services for handling this cross-cutting functionality and they can be maintained independently.
  • When your team is huge and you want specialized teams to handle, manage, and maintain core cross-cutting functionalities.
  • You need to support the latest connectivity options in a legacy application or an application that is difficult to change.
  • You want to monitor resource consumption across the application and cut off a microservice if its resource consumption is huge.
  • When not to use: While this pattern does solve many issues, this pattern is not recommended in the following scenarios:
  • When network latency is utmost. Introducing a proxy layer would introduce some overhead that will create a slight delay, which may not be good for real-time scenarios.
  • When connectivity features cannot be generalized and require another level of integration and dependency with another service.
  • When creating a client library and distributing it to the microservices development team as a package.
  • For small applications where introducing an extra layer is actually an overhead.
  • When some services need to scale independently; if so, then the better alternative would be to deploy it separately and independently. 

Anti-corruption microservice design pattern

Often, we need interoperability or coexistence between legacy and modern applications. This design provides an easy solution for this by introducing a facade between modern and legacy applications. This design pattern ensures that the design of an application is not hampered or blocked by legacy system dependencies:

  • Problem: New systems or systems in the process of migration often need to communicate with the legacy system. The new system's model and technology would probably be different, considering that old systems are usually weak, but still, legacy resources may be needed for some operations. Often, these legacy systems suffer from poor design and poor schema designs. For interoperability, we may still need to support the old system. This pattern is about solving such corruption and still have a cleaner, neater, and easier to maintain microservice ecosystem.

  • Solution: To avoid using legacy code or a legacy system, design a layer that does the following task: acts as the only layer for communicating with legacy code, which prevents accessing legacy code directly wherein different people may deal with them differently. The core concept is to separate out a legacy or the corrupt application by placing an ACL with the objective of not changing the legacy layer, and thus avoid compromising its approach or major technological change.

  • The anti-corruption layer (ACL) should contain all the logic for translating as per new needs from the old model. This layer can be introduced as a separate service or translator component any place where needed. A general approach to organizing the design of the ACL is a combination of a facade, adapters, translators, and communicators to talk to systems. An ACL is used to prevent unexpected behavior of an external system from leaking in your existing context:

  • Take care of: While effectively implementing this pattern, the following points should be considered as they are some of the major pitfalls:
  • The ACL should be properly scaled and given a better resources pool, as it will add latency to calls made between two systems.
  • Make sure that the corruption layer you introduce is actually an improvement and you don't introduce yet another layer of corruption.
  • The ACL adds an extra service; hence it must be effectively managed and maintained and scaled.
  • Effectively decide the number of ACLs. There can be many reasons to introduce an ACL—a means to translate undesirable formats of the object in required formats means to communicate between different languages, and so on.
  • Effective measures to make sure that transactions and data consistency are maintained between both systems and can be monitored.
  • The duration of the ACL, will it be permanent, how will the communication be handled.
  • While an ACL should successfully handle exceptions from the corruption layer, it should not completely, otherwise it would be very difficult to preserve any information about the original error.
  • When to use: The anti-corruption pattern is highly recommended and extremely useful in the following scenarios:
  • There is a huge system up for refactoring from monolithic to microservices and there is a phase-by-phase migration planned instead of the big bang migration wherein the legacy system and new system need to coexist and communicate with each other.
  • If the system that you are undertaking is dealing with any data source whose model is undesirable or not in sync with the needed model, this pattern can be introduced and it will do the task of translating from undesirable formats to needed formats.
  • Whenever there is a need to link two bounded contexts, that is, a system is developed by someone else entirely and there is very little understanding of it, this pattern can be introduced as a link between systems.
  • When not to use: This pattern is highly not recommended in the following scenarios:
  • There are no major differences between new and legacy systems. The new system can coexist without the legacy system.
  • You have lots of transactional operations and maintaining data consistency between the ACL and the corrupt layer adds too much latency. In such case, this pattern can be merged with other patterns.
  • Your organization doesn't have extra teams to maintain and scale the ACL as and when needed.

Bulkhead design pattern

Separate out different services in the microservices application into various pools such that if one of the services fails, the others will continue to function irrespective of failure. Create a different pool for each microservice to minimize the impact:

  • Problem: This pattern takes inspiration from sectioned out parts of a ship's hull. If the hull of a ship is damaged, then only the damaged section would fill with water, which would prevent the ship from sinking. Let's say you are connecting to various microservices that are using a common thread pool. If one of the services starts showing delay, then all pool members will be too exhausted to wait for responses. Incrementally, a large number of requests coming from one service would deplete available resources. That's where this pattern suggests a dedicated pool for every single service.
  • Solution: Separate service instances into different groups based on load and network usage. This allows you to isolate system failures and prevent depletion of resources in the connection pool. The essential advantages of this system are the prevention of propagating failures and ability to configure capacity of the resource pool. For higher priority services, you may assign higher pools.

Note

For example, given is a sample file from which we can see pool allocation for service shopping-management: https://gist.github.com/parthghiya/be80246cc5792f796760a0d43af935db.

  • Take care of: Make sure to take care of the following points to make sure that a proper bulkhead design is implemented:
  • Define proper independent partitions in the application based on business and technical requirements.
  • Bulkheads can be introduced in forms of thread pools and processes. Decide which one is suitable for your application.
  • Isolation in the deployment of your microservices.
  • When to use: The bulkhead pattern adds an advantage in the following scenarios:
  • The application is huge and you want to protect it from cascading or spreading failures
  • You can isolate critical services from standard services and you can allocate separate pools for them
  • When not to use: This pattern is not advisable for the following scenarios:
  • When you don't have that much budget for maintaining separate overheads in terms of cost and management
  • The added level of complexity of maintaining separate pools is not necessary
  • Your resources usage is unexpected and you can't isolate your tenants and keep a limit on it as it is not acceptable when you would place several tenants in one partition

Circuit breaker

Services sometimes need to collaborate with each other when they need to handle requests. In such cases, there is a very high scenario that the other service is not available, is showing high latency, or is unusable. This pattern essentially solves this issue by introducing a breakage in the circuit that stops propagation in the whole architecture:

  • Problem: In the microservices architecture when there is inter-services communication, a remote call needs to be invoked instead of an in-memory call. It may so happen that the remote call may fail or reach a timeout limit and hang without any response. Now in such cases when there are many callers, then all such locked threads that you can run out of resources and the whole system will become unresponsive.
  • Solution: A very primitive idea for solving this issue is introducing a wrapper for a protected function call, it monitors for failures. Now this wrapper can be triggered via anything such as certain threshold in failures, database connection fails, and so on. All further calls will return with an error and stop catastrophic propagation. This will trip the circuit open, and while the circuit is open, it will avoid making the protected call. The implementation is done in the following three stages just as in an electric circuit. It is in three stages: Closed State, Open State, and Half-Open State, as explained in the following diagram:

Note

Here is an example for implementation in Node.js: Hystrix is open sourced by Netflix https://gist.github.com/parthghiya/777c2b423c9c8faf0d427fd7a3eeb95b

  • Take care of: The following needs to be taken care of when you want to apply the circuit breaker pattern:
  • Since you are invoking a remote call, and there may be many remote call invocation asynchronous and reactive principles for using future, promises, async, and await is must.
  • Maintain a queue of requests; when your queue is overcrowded, you can easily trip the circuit. Always monitor the circuit, as you will often need to activate it again for an efficient system. So, have a mechanism ready for reset and failure handlers.
  • You have a persistent storage and network cache such as Memcache or Redis to record availability.
  • Logging, exception handling, and relaying failed requests.
  • When to use: In the following use cases, you can use the circuit breaker pattern:
  • When you don't want your resources to be depleted, that is, an action that is doomed to fail shouldn't be tried until it is fixed. You can use it to check the availability of external services.
  • When you can compromise a bit on performance, but want to gain high availability of the system and not deplete resources.
  • When not to use: In the following scenarios, it is not advisable to introduce the circuit breaker pattern:
  • You don't have an efficient cache layer that monitors and maintains states of services for a given time for requests across the clustered nodes.
  • For handling in-memory structures or as the substitute for handling exceptions in business logic. This would add overhead to performance.

Strangler pattern

Today's world is one where technology is constantly evolving. What is written today, is just tomorrow's legacy code. This pattern is very helpful when it comes to the migration process. This pattern is about eventually migrating a legacy system by incrementally replacing particular parts of functionality with new microservices application and services. It eventually introduces a proxy that redirects either to the legacy or the new microservices, until the migration is complete and at the end, you can shut off the strangler or the proxy:

  • Problem: With aging systems, new evolving development tools, and hosting options, the evolution of cloud and serverless platforms maintaining the current system gets extremely painful with the addition of new features and functionalities. Completely replacing a system can be a huge task, for which gradual migration needs to be done such that the old system is still handled for the part that hasn't been migrated yet. This pattern solves this very problem.
  • Solution: The strangler solution resembles a vine that strangles a tree that it's wrapped over. Over time, the migrated application strangles the original application until you can shut off the monolithic application. Thus, the overall process is as follows:
  • Reconstruct: Construct a new application or site (in serverless or AWS cloud-based on modern principles). Incrementally reconstruct the functionalities in an agile manner.
  • Coexist: Leave the legacy application as it is. Introduce a facade that eventually acts as a proxy and decides where to route the request based on the current migration status. This facade can be introduced at web server level or programming level based on various parameters such as IP address, user agent, or cookies.
  • Terminate: Redirect everything to the modern migrated application and loosen off all the ties with the legacy application.

Note

A sample gist of .htaccess that acts as a facade can be found at this link: https://gist.github.com/parthghiya/a6935f65a262b1d4d0c8ac24149ce61d.

The solution instructs us to create a facade or a proxy that has the ability to intercept the requests that are going to the backend legacy system. The facade or proxy then decides whether to route it to the legacy application or the new microservices. This is an incremental process, wherein both the systems can coexist. The end users won't even know when the migration process is complete. It gives the added advantage that if the adopted microservice approach doesn't work, there is a very simple way to change it.

  • Take care of: The following points need to be taken care of for effectively applying the strangulation pattern:
  • The facade or the proxy needs to be updated with the migration.
  • The facade or the proxy shouldn't be a single point of failure or bottleneck.
  • When the migration is complete, facade will evolve as the adapter for legacy applications.
  • The new code written should be such that it can easily be intercepted, so in future, we can replace it in future migrations.
  • When to use: The strangler application is extremely useful when it comes to replacing a legacy and monolithic application with microservices. The pattern is used in the following cases:
  • When you want to follow the test-driven on behavior-driven development, and run fast and comprehensive tests with the accessibility of code coverages and adapt CI/CD.
  • Your application can be contained bounded contexts within which a model applies in the region. As an example, in a shopping cart application, the product module would be one context.
  • When not to use: This pattern may not be applicable in the following scenarios:
  • When you are not able to intercept the user agent request, or you are not able to introduce a facade in your architecture.
  • When you think of doing a page by page migration at a time or you are thinking of doing it all at once.
  • When your application is more frontend-driven; that's where you have to entirely change and rework the interacting framework based on the way the frontend is interacting with services, as you don't want to expose the various ways the user agent is interacting with services.