A microservices architecture is a development method for designing applications as modular services that seamlessly adapt to a highly scalable and dynamic environment. Microservices help solve complex issues such as speed and scalability, while also supporting continuous testing and delivery. This Zone will take you through breaking down the monolith step by step and designing a microservices architecture from scratch. Stay up to date on the industry's changes with topics such as container deployment, architectural design patterns, event-driven architecture, service meshes, and more.
This is an article from DZone's 2023 Software Integration Trend Report.For more: Read the Report Our approach to scalability has gone through a tectonic shift over the past decade. Technologies that were staples in every enterprise back end (e.g., IIOP) have vanished completely with a shift to approaches such as eventual consistency. This shift introduced some complexities with the benefit of greater scalability. The rise of Kubernetes and serverless further cemented this approach: spinning a new container is cheap, turning scalability into a relatively simple problem. Orchestration changed our approach to scalability and facilitated the growth of microservices and observability, two key tools in modern scaling. Horizontal to Vertical Scaling The rise of Kubernetes correlates with the microservices trend as seen in Figure 1. Kubernetes heavily emphasizes horizontal scaling in which replications of servers provide scaling as opposed to vertical scaling in which we derive performance and throughput from a single host (many machines vs. few powerful machines). Figure 1: Google Trends chart showing correlation between Kubernetes and microservice (Data source: Google Trends ) In order to maximize horizontal scaling, companies focus on the idempotency and statelessness of their services. This is easier to accomplish with smaller isolated services, but the complexity shifts in two directions: Ops – Managing the complex relations between multiple disconnected services Dev – Quality, uniformity, and consistency become an issue. Complexity doesn't go away because of a switch to horizontal scaling. It shifts to a distinct form handled by a different team, such as network complexity instead of object graph complexity. The consensus of starting with a monolith isn't just about the ease of programming. Horizontal scaling is deceptively simple thanks to Kubernetes and serverless. However, this masks a level of complexity that is often harder to gauge for smaller projects. Scaling is a process, not a single operation; processes take time and require a team. A good analogy is physical traffic: we often reach a slow junction and wonder why the city didn't build an overpass. The reason could be that this will ease the jam in the current junction, but it might create a much bigger traffic jam down the road. The same is true for scaling a system — all of our planning might make matters worse, meaning that a faster server can overload a node in another system. Scalability is not performance! Scalability vs. Performance Scalability and performance can be closely related, in which case improving one can also improve the other. However, in other cases, there may be trade-offs between scalability and performance. For example, a system optimized for performance may be less scalable because it may require more resources to handle additional users or requests. Meanwhile, a system optimized for scalability may sacrifice some performance to ensure that it can handle a growing workload. To strike a balance between scalability and performance, it's essential to understand the requirements of the system and the expected workload. For example, if we expect a system to have a few users, performance may be more critical than scalability. However, if we expect a rapidly growing user base, scalability may be more important than performance. We see this expressed perfectly with the trend towards horizontal scaling. Modern Kubernetes systems usually focus on many small VM images with a limited number of cores as opposed to powerful machines/VMs. A system focused on performance would deliver better performance using few high-performance machines. Challenges of Horizontal Scale Horizontal scaling brought with it a unique level of problems that birthed new fields in our industry: platform engineers and SREs are prime examples. The complexity of maintaining a system with thousands of concurrent server processes is fantastic. Such a scale makes it much harder to debug and isolate issues. The asynchronous nature of these systems exacerbates this problem. Eventual consistency creates situations we can't realistically replicate locally, as we see in Figure 2. When a change needs to occur on multiple microservices, they create an inconsistent state, which can lead to invalid states. Figure 2: Inconsistent state may exist between wide-sweeping changes Typical solutions used for debugging dozens of instances don't apply when we have thousands of instances running concurrently. Failure is inevitable, and at these scales, it usually amounts to restarting an instance. On the surface, orchestration solved the problem, but the overhead and resulting edge cases make fixing such problems even harder. Strategies for Success We can answer such challenges with a combination of approaches and tools. There is no "one size fits all," and it is important to practice agility when dealing with scaling issues. We need to measure the impact of every decision and tool, then form decisions based on the results. Observability serves a crucial role in measuring success. In the world of microservices, there's no way to measure the success of scaling without such tooling. Observability tools also serve as a benchmark to pinpoint scalability bottlenecks, as we will cover soon enough. Vertically Integrated Teams Over the years, developers tended to silo themselves based on expertise, and as a result, we formed teams to suit these processes. This is problematic. An engineer making a decision that might affect resource consumption or might impact such a tradeoff needs to be educated about the production environment. When building a small system, we can afford to ignore such issues. Although as scale grows, we need to have a heterogeneous team that can advise on such matters. By assembling a full-stack team that is feature-driven and small, the team can handle all the different tasks required. However, this isn't a balanced team. Typically, a DevOps engineer will work with multiple teams simply because there are far more developers than DevOps. This is logistically challenging, but the division of work makes more sense in this way. As a particular microservice fails, responsibilities are clear, and the team can respond swiftly. Fail-Fast One of the biggest pitfalls to scalability is the fail-safe approach. Code might fail subtly and run in non-optimal form. A good example is code that tries to read a response from a website. In a case of failure, we might return cached data to facilitate a failsafe strategy. However, since the delay happens, we still wait for the response. It seems like everything is working correctly with the cache, but the performance is still at the timeout boundaries. This delays the processing. With asynchronous code, this is hard to notice and doesn't put an immediate toll on the system. Thus, such issues can go unnoticed. A request might succeed in the testing and staging environment, but it might always fall back to the fail-safe process in production. Failing fast includes several advantages for these scenarios: It makes bugs easier to spot in the testing phase. Failure is relatively easy to test as opposed to durability. A failure will trigger fallback behavior faster and prevent a cascading effect. Problems are easier to fix as they are usually in the same isolated area as the failure. API Gateway and Caching Internal APIs can leverage an API gateway to provide smart load balancing, caching, and rate limiting. Typically, caching is the most universal performance tip one can give. But when it comes to scale, failing fast might be even more important. In typical cases of heavy load, the division of users is stark. By limiting the heaviest users, we can dramatically shift the load on the system. Distributed caching is one of the hardest problems in programming. Implementing a caching policy over microservices is impractical; we need to cache an individual service and use the API gateway to alleviate some of the overhead. Level 2 caching is used to store database data in RAM and avoid DB access. This is often a major performance benefit that tips the scales, but sometimes it doesn't have an impact at all. Stack Overflow recently discovered that database caching had no impact on their architecture, and this was because higher-level caches filled in the gaps and grabbed all the cache hits at the web layer. By the time a call reached the database layer, it was clear this data wasn't in cache. Thus, they always missed the cache, and it had no impact. Only overhead. This is where caching in the API gateway layer becomes immensely helpful. This is a system we can manage centrally and control, unlike the caching in an individual service that might get polluted. Observability What we can't see, we can't fix or improve. Without a proper observability stack, we are blind to scaling problems and to the appropriate fixes. When discussing observability, we often make the mistake of focusing on tools. Observability isn't about tools — it's about questions and answers. When developing an observability stack, we need to understand the types of questions we will have for it and then provide two means to answer each question. It is important to have two means. Observability is often unreliable and misleading, so we need a way to verify its results. However, if we have more than two ways, it might mean we over-observe a system, which can have a serious impact on costs. A typical exercise to verify an observability stack is to hypothesize common problems and then find two ways to solve them. For example, a performance problem in microservice X: Inspect the logs of the microservice for errors or latency — this might require adding a specific log for coverage. Inspect Prometheus metrics for the service. Tracking a scalability issue within a microservices deployment is much easier when working with traces. They provide a context and a scale. When an edge service runs into an N+1 query bug, traces show that almost immediately when they're properly integrated throughout. Segregation One of the most important scalability approaches is the separation of high-volume data. Modern business tools save tremendous amounts of meta-data for every operation. Most of this data isn't applicable for the day-to-day operations of the application. It is meta-data meant for business intelligence, monitoring, and accountability. We can stream this data to remove the immediate need to process it. We can store such data in a separate time-series database to alleviate the scaling challenges from the current database. Conclusion Scaling in the age of serverless and microservices is a very different process than it was a mere decade ago. Controlling costs has become far harder, especially with observability costs which in the case of logs often exceed 30 percent of the total cloud bill. The good news is that we have many new tools at our disposal — including API gateways, observability, and much more. By leveraging these tools with a fail-fast strategy and tight observability, we can iteratively scale the deployment. This is key, as scaling is a process, not a single action. Tools can only go so far and often we can overuse them. In order to grow, we need to review and even eliminate unnecessary optimizations if they are not applicable. This is an article from DZone's 2023 Software Integration Trend Report.For more: Read the Report
Service meshes are becoming increasingly popular in cloud-native applications as they provide a way to manage network traffic between microservices. Istio, one of the most popular service meshes, uses Envoy as its data plane. However, to maintain the stability and reliability of modern web-scale applications, organizations need more advanced load management capabilities. This is where Aperture comes in. It offers several features, including: Prioritized load shedding: Drops traffic that is deemed less important to ensure that the most critical traffic is served. Distributed rate-limiting: Prevents abuse and protects the service from excessive requests. Intelligent autoscaling: Adjusts resource allocation based on demand and performance. Monitoring and telemetry: Continuously monitors service performance and request attributes using an in-built telemetry system. Declarative policies: Provides a policy language that enables teams to define how to react to different situations. These capabilities help manage network traffic in a microservices architecture, prioritize critical requests, and ensure reliable operations at scale. Furthermore, the integration with Istio for flow control is seamless and without the need for application code changes. In this blog post, we will dive deeper into what a Service Mesh is, the role of Istio and Envoy, and how they work together to provide traffic management capabilities. Finally, we will show you how to manage loads with Aperture in an Istio-configured environment. What Is a Service Mesh? Since the advent of microservices architecture, managing and securing service-to-service communication has been a significant challenge. As the number of microservices grows, the complexity of managing and securing the communication between them increases. In recent years, a new approach has emerged that aims to address these challenges: the service mesh. The concept of a service mesh was first introduced in 2016 when a team of engineers from Buoyant, a startup focused on cloud-native infrastructure, released Linkerd, an open-source service mesh for cloud-native applications. Linkerd was designed to be lightweight and unobtrusive, providing a way to manage service-to-service communication without requiring significant changes to the application code. Istio as a Service Mesh Source: Istio Istio is an open-source service mesh that is designed to manage and secure communication between microservices in cloud-native applications. It provides a number of features for managing and securing service-to-service communication, including traffic management, security, and observability. At its core, Istio works by deploying a sidecar proxy alongside each service instance. This proxy intercepts all incoming and outgoing traffic for the service and provides a number of capabilities, including traffic routing, load balancing, service discovery, security, and observability. When a service sends a request to another service, the request is intercepted by the sidecar proxy, which then applies a set of policies and rules that are defined in Istio's configuration. These policies and rules dictate how traffic should be routed, how the load should be balanced, and how security should be enforced. For example, Istio can be used to implement traffic routing rules based on request headers, such as routing requests from a specific client to a specific service instance. Istio can also be used to apply circuit-breaking and rate-limiting policies to ensure that a single misbehaving service does not overwhelm the entire system. Istio also provides strong security capabilities, including mutual TLS authentication between services, encryption of traffic between services, and fine-grained access control policies that can be used to restrict access to services based on user identity, IP address, or other factors. What Is Envoy? Envoy is an open-source, high-performance edge and service proxy developed by Lyft. It was created to address the challenges of modern service architectures, such as microservices, cloud-native computing, and containerization. Envoy provides a number of features for managing and securing network traffic between services, including traffic routing, load balancing, service discovery, health checks, and more. Envoy is designed to be a universal data plane that can be used with a wide variety of service meshes and API gateways, and it can be deployed as a sidecar proxy alongside service instances. One of the key benefits of Envoy is its high-performance architecture, which makes it well-suited for managing large volumes of network traffic in distributed systems. Envoy uses a multi-threaded, event-driven architecture that can handle tens of thousands of connections per host with low latency and high throughput. Traffic Management in Istio Istio's traffic management capabilities enable the control of network traffic flow between microservices. This refers to the ability to manage how the traffic flows between them. In Istio, Traffic management is primarily done through the use of Envoy sidecar proxies that are deployed alongside your microservices. Envoy proxies are responsible for handling incoming and outgoing network traffic and can perform a variety of functions such as load balancing, routing, and security. Let's talk about EnvoyFilter and how it is used in Aperture. Aperture's EnvoyFilter One way to customize the behavior of Envoy proxies in Istio is through the use of Envoy filters. Envoy filters are a powerful feature of Envoy that allow you to modify, route, or terminate network traffic based on various conditions such as HTTP headers, request/response bodies, or connection metadata. In Istio, Envoy filters can be added to your Envoy sidecar proxies by creating custom EnvoyFilter resources in Kubernetes. These resources define the filter configuration, the filter type, and the filter order, among other parameters. There are several types of filters that can be used in Istio, including HTTP filters, network filters, and access log filters. Here are some of the Envoy filters that Apertures uses. HTTP filters: You can use HTTP filters to modify or route HTTP traffic based on specific criteria. For example, you can add an HTTP filter to strip or add headers, modify request/response bodies, or route traffic to different services based on HTTP headers or query parameters. Network filters: You can use network filters to modify network traffic at the transport layer, such as TCP or UDP. For example, you can use a network filter to add or remove SSL/TLS encryption or to redirect traffic to a different IP address. Filters are loaded dynamically into Envoy and can be applied globally to all traffic passing through the proxy or selectively to specific services or routes. Aperture uses EnvoyFilter to implement its flow control capabilities. The Aperture Agent is integrated with Envoy using EnvoyFilter, which allows the agent to use the External authorization API (we will learn more about this in the next section). This API allows Aperture to extract metadata from requests and makes flow control decisions based on that metadata. With EnvoyFilter, Aperture can intercept, inspect, and modify the traffic flowing through Envoy, providing more advanced and flexible flow control capabilities. External Authorization API The External Authorization API is a feature provided by Envoy Proxy that allows external authorization services to make access control decisions based on metadata extracted from incoming requests. It provides a standard interface for Envoy to call external authorization services for making authorization decisions, which allows for a more flexible and centralized authorization control for microservices. Control Using Envoy's External Authorization API enables Aperture to make flow control decisions based on a variety of request metadata beyond basic authentication and authorization. By extracting and analyzing data from request headers, paths, query parameters, and other attributes, Aperture can gain a more comprehensive understanding of the flow of traffic between microservices. This capability allows Aperture to prioritize critical requests over others, ensuring reliable operations at scale. Moreover, by utilizing Aperture flow control, applications can degrade gracefully in real-time, meaning Aperture can prioritize the different workloads. Aperture uses Envoy's External Authorization definition to describe request metadata, specifically the AttributeContext. However, the ability to extract values from the request body depends on how External Authorization in Envoy was configured. Access Logs In addition to flow control, Aperture also provides access logs that developers can use to gain insight into the flow of network traffic in their microservices architecture. These logs capture metadata about each request, such as the request method, path, headers, and response status code, which can be used to optimize application performance and reliability. By analyzing traffic patterns and identifying potential issues or performance bottlenecks, developers can make informed decisions to improve their microservices architecture. Aperture extract fields from access logs containing high-cardinality attributes that represent key attributes of requests and features. You can find all these extracted fields here. These fields allow for a detailed analysis of system performance and behavior and provide a comprehensive view of individual requests or features within services. Additionally, this data can be stored and visualized using FluxNinja ARC, making it available for querying using an OLAP backend (Druid). Let’s jump into the implementation of Aperture with Istio. Flow Control Using Aperture With Istio Load management is important in web-scale applications to ensure stability and reliability. Aperture provides flow control mechanisms such as weighted fair queuing, distributed rate-limiting, and prioritization of critical features to regulate the flow of requests and prevent overloading. These techniques help to manage the flow of requests and enable load management. Demo Prerequisites The following are prerequisites before going ahead with Aperture Flow Control Integration with Istio. A Kubernetes cluster: For the purposes of this demo, you can use the Kubernetes-in-Docker (Kind) environment. Aperture Controller and Aperture Agent installed in your Kubernetes cluster. Istio is installed in your Kubernetes cluster. Step 1: Install Aperture Controller and Aperture Agent To install Aperture Controller and Aperture Agent running as Daemon Set in your Kubernetes cluster, follow the instructions provided in the Aperture documentation. Step 2: Install Istio To install Istio in your Kubernetes cluster, follow the instructions provided in the Istio documentation. Step 3: Enable Sidecar Injection To enable Istio sidecar injection for your applications deployments in a specific namespace, run the following command: kubectl label namespace <namespace-name> istio-injection=enabled This will add the istio-injection=enabled label to the specified namespace, allowing Istio to inject sidecars automatically into the pods. Example: kubectl create namespace demoapp kubectl label namespace demoapp istio-injection=enabled Step 4: Apply Envoy Filters Patches To configure Aperture with Istio, four patches have to be applied via Envoy Filter. These patches essentially serve the following purposes: To merge the values extracted from the filter with the Open Telemetry Access Log configuration to the HTTP Connection Manager filter for the outbound listener in the Istio sidecar running with the application. This is called the NETWORK_FILTER Patch. To merge the Open Telemetry Access Log configuration with the HTTP Connection Manager filter for the inbound listener in the Istio sidecar running with the application. This is also a NETWORK_FILTER Patch. To insert the External Authorization before the Router sub-filter of the HTTP Connection Manager filter for the inbound listener in the Istio sidecar running with the application. This is called the HTTP_FILTER Patch. To insert the External Authorization before the Router sub-filter of the HTTP Connection Manager filter, but for the outbound listener in the Istio sidecar running with the application. This is another HTTP_FILTER Patch. In simpler terms, the patches are applied to the Istio sidecar running alongside the application. They help to modify the HTTP Connection Manager filter, which manages incoming and outgoing traffic to the application. The patches enable Aperture to extract metadata, such as request headers, paths, and query parameters, to make flow control decisions for each request. This allows Aperture to prioritize critical requests over others, ensure reliable operations at scale, and prevent overloading the application. Refer to Aperture Envoy Configuration documentation to know more about each patch. For convenience, Aperture provides two ways to do this, Helm: Using Aperture istioconfig Helm chart Aperturectl: Own Aperture cli provides a way to do istioconfig install in ISTIOD namespace. Using Helm: helm repo add aperture https://fluxninja.github.io/aperture/ helm repo update helm upgrade --install aperture-envoy-filter aperture/istioconfig --namespace ISTIOD_NAMESPACE_HERE Using aperturectl aperturectl install istioconfig --version v0.26.1 --namespace ISTIOD_NAMESPACE_HERE Example: aperturectl install istioconfig --version v0.26.1 --namespace istio-system The default values for the Aperture Agent service namespace are aperture-agent, the port is 8080, and sidecar mode is false. This means that the Aperture Agent target URL is aperture-agent.aperture-agent.svc.cluster.local:8080. If you have installed the Aperture Agent in a different namespace or port, you can create or update the values.yaml file and pass it with the install command. Please refer to the Aperture documentation for more information on how to configure Aperture for custom values and how to create or update the values.yaml file and pass it with the install command. Step 5: Verify Integration kubectl get envoyfilter aperture-envoy-filter -n ISTIOD_NAMESPACE_HERE Example Command: kubectl get envoyfilter aperture-envoy-filter -n istio-system Output: NAME AGE aperture-envoy-filter 46s Deploy Demo-App To demonstrate whether Istio integration is working, install a demo deployment in demoapp namespace with istio-injection enabled. kubectl create namespace demoapp kubectl label namespace demoapp istio-injection=enabled kubectl create -f https://gist.githubusercontent.com/sudhanshu456/10b8cfd09629ae5bbce9900d8055603e/raw/cfb8f0850cc91364c1ed27f6904655d9b338a571/demoapp-and-load-generator.yaml -n demoapp Check pods are up and running. kubectl get pods -n demoapp Output: NAME READY STATUS RESTARTS AGE service1-demo-app-686bbb9f64-rhmpp 2/2 Running 0 14m service2-demo-app-7f8dd75749-zccx8 2/2 Running 0 14m service3-demo-app-6899c6bc6b-r4mpd 2/2 Running 0 14m wavepool-generator-7bd868947d-fmn2w 1/1 Running 0 2m36s Apply Policy Using aperturectl To demonstrate control points and their functionality, we will use a basic concurrency limiting policy. Below is the values.yaml file we will apply using aperturectl. values.yaml common: policy_name: "basic-concurrency-limiting" policy: flux_meter: flow_selector: service_selector: agent_group: default service: service1-demo-app.demoapp.svc.cluster.local flow_matcher: control_point: ingress concurrency_controller: flow_selector: service_selector: agent_group: default service: service1-demo-app.demoapp.svc.cluster.local flow_matcher: control_point: ingress Use the below command to apply the above policy values.yaml. aperturectl blueprints generate --name=policies/latency-aimd-concurrency-limiting --values-file values.yaml --output-dir=policy-gen --version=v0.26.1 --apply Validate Policy Let’s check if the policy is applied or not. kubectl get policy -n aperture-controller Output: NAME STATUS AGE basic-concurrency-limiting uploaded 20s List Active Control Points Using aperturectl Control points are the targets for flow control decision-making. By defining control points, Aperture can regulate the flow of traffic between services by analyzing request metadata, such as headers, paths, and query parameters. Aperture provides a command-line interface called aperturectl that allows you to view active control points and live traffic samples. Using aperturectl flow-control control-points, you can list active control points. aperturectl flow-control control-points --kube Output: AGENT GROUP SERVICE NAME default egress service1-demo-app.demoapp.svc.cluster.local default egress service2-demo-app.demoapp.svc.cluster.local default ingress service1-demo-app.demoapp.svc.cluster.local default ingress service2-demo-app.demoapp.svc.cluster.local default ingress service3-demo-app.demoapp.svc.cluster.local Live Previewing Requests Using aperturectl aperturectl provides a flow-control preview feature that enables you to view live traffic and visualize it. This feature helps you understand the incoming request attributes, which is an added benefit on top of Istio. You can use this feature to preview incoming traffic and ensure that Aperture is making the correct flow control decisions based on the metadata extracted from the request headers, paths, query parameters, and other attributes. aperturectl flow-control preview --kube service1-demo-app.demoapp.svc.cluster.local ingress --http --samples 1 Output: { "samples": [ { "attributes": { "destination": { "address": { "socketAddress": { "address": "10.244.1.11", "portValue": 8099 } } }, "metadataContext": {}, "request": { "http": { "headers": { ":authority": "service1-demo-app.demoapp.svc.cluster.local", ":method": "POST", ":path": "/request", ":scheme": "http", "content-length": "201", "content-type": "application/json", "cookie": "session=eyJ1c2VyIjoia2Vub2JpIn0.YbsY4Q.kTaKRTyOIfVlIbNB48d9YH6Q0wo", "user-agent": "k6/0.43.1 (https://k6.io/)", "user-id": "52", "user-type": "subscriber", "x-forwarded-proto": "http", "x-request-id": "a8a2684d-9acc-4d28-920d-21f2a86c0447" }, "host": "service1-demo-app.demoapp.svc.cluster.local", "id": "14105052106838702753", "method": "POST", "path": "/request", "protocol": "HTTP/1.1", "scheme": "http" }, "time": "2023-03-15T11:50:44.900643Z" }, "source": { "address": { "socketAddress": { "address": "10.244.1.13", "portValue": 33622 } } } }, "parsed_body": null, "parsed_path": [ "request" ], "parsed_query": {}, "truncated_body": false, "version": { "encoding": "protojson", "ext_authz": "v3" } } ] } What's Next? Generating and Applying Policies: Check out our tutorials on flow-control and load-management techniques here. Conclusion In this blog post, we have explored the concept of a service mesh, which is a tool used to manage the communication between microservices in a distributed system. We have also discussed how Aperture can integrate with Istio, an open-source service mesh framework, to provide enhanced load management capabilities. By following a few simple steps, you can get started with using these load management techniques, which include concurrency limiting, dynamic rate limiting, and prioritized load shedding, to improve the performance and reliability of your microservices. To learn more about Aperture, please visit our GitHub repository and documentation site. You can also join our Slack community to discuss best practices, ask questions, and engage in discussions on reliability management.
Microservices architecture has gained popularity recently as a technique for creating sophisticated and scalable software systems. Microservices are scalable, independently deployable services that talk to one another across a network. Making it easier for these services to communicate with one another is one of the major problems with a microservices design. HTTP and messaging are two popular methods for microservices communication. The common protocol used for communication between web servers and clients is called HTTP (Hypertext Transfer Protocol). HTTP is frequently utilized as a means of communication between services in a microservices architecture. On the other side, messaging involves the exchange of messages between services utilizing a messaging system such as RabbitMQ, Apache Kafka, or Amazon SQS. The decision between HTTP and messaging depends on a number of variables, including the particular use case, scalability requirements, and system complexity. Both protocols have advantages and disadvantages. Understanding the benefits and drawbacks of each strategy is crucial in this situation to choose the best course of action. For microservices communication, this article will examine the distinctions between HTTP and messaging and give a general overview of the trade-offs involved with each strategy. Communication Using HTTP HTTP is the World Wide Web’s basis and is extensively used for communication between web browsers and servers. In recent years, it has also been employed as a means of communication among microservices. RESTful APIs are used to exchange data across microservices in this technique. Let’s have a look at some code to observe how HTTP-based communication varies from messaging-based communication in microservices: Java public class OrderService { private RestTemplate restTemplate; public OrderService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public void processOrder(Order order) { Customer customer = restTemplate.getForObject("http://customer-service/customers/" + order.getCustomerId(), Customer.class); // process order logic... } } In this example, the order microservice makes an HTTP request to the customer microservice to get customer information using a RestTemplate. The customer data is retrieved using the getForObject function, and the response is deserialized into a Customer object. We have two microservices, a customer microservice, and an order microservice. The order microservice may get client information thanks to the customer microservice’s RESTful API. Benefits of HTTP-Based Communication Simple to use: HTTP is a well-known and simple protocol. Developers may readily create RESTful APIs to expose microservices and enable communication between them. Statelessness: Because of its statelessness, HTTP is naturally scalable. Each request is handled individually, and no connection between the client and server is required. Caching: HTTP allows for caching, which is important when dealing with huge volumes of data. Microservices can reduce system load and enhance speed by caching frequently requested data. Disadvantages of HTTP-Based Communication Latency: When significant volumes of data are exchanged, an HTTP-based connection might create lag. This can result in slower reaction times and reduced performance. Complexity: As the number of endpoints and methods rises, RESTful APIs may become complicated. This can make system maintenance and updates difficult. Lack of dependability: HTTP-based communication is network-dependent and can be impacted by network slowness and errors. Communication via Messaging A message broker is used to promote communication between microservices in messaging-based communication. Messages are routed across a queue between microservices, decoupling the sender and recipient. We have the same two microservices in this example: Customer microservice Order microservice The order microservice subscribes to this information once the customer microservice publishes it to a message broker. Customer Microservice Identifier Java public class CustomerService { private MessageBroker messageBroker; public CustomerService(MessageBroker messageBroker) { this.messageBroker = messageBroker; } public void createCustomer(Customer customer) { // create customer logic... messageBroker.publish("customer.created", customer); } } Order Microservice Code Java public class OrderService { private MessageBroker messageBroker; public OrderService(MessageBroker messageBroker) { this.messageBroker = messageBroker; this.messageBroker.subscribe("customer.created", this::processOrder); } private void processOrder(Customer customer) { // process order logic... } } When a new customer is established, the customer microservice publishes customer information to the message broker. The order microservice uses the subscribe method to subscribe to this information, and the processOrder method is invoked whenever fresh client information is received. Benefits of Messaging-Based Communication Scalability: Messaging-based communication is extremely scalable and capable of handling massive volumes of data with little delay. This makes it perfect for high-throughput applications. Dependability: Because of the employment of a message broker, messaging-based communication is extremely dependable. Communications are queued until they are properly processed, reducing the chance of data loss. Adaptability: Messaging-based communication is adaptable and can handle a broad range of data forms, including binary data. As a result, it is appropriate for applications that work with complicated data structures. Disadvantages of Messaging-Based Communication Complexity: Messaging-based communication can be difficult to set up and manage, especially when numerous message brokers are involved. Protocol support is limited: Messaging-based communication is frequently restricted to a few protocols, such as AMQP or MQTT. This can make integration with other systems that utilize other protocols problematic. Lack of standardization: There is presently no common message protocol for microservices communication, making interoperability between various systems problematic. As in the examples, there are substantial differences between HTTP-based and messaging-based communication in microservices. HTTP-based communication is straightforward and quick to use, but as the number of endpoints and methods grows, it can create delay and complexity. Although messaging-based communication is incredibly scalable and reliable, it is more complicated to set up and operate. Eventually, the choice between these two approaches is dictated by the application’s specific requirements. Conclusion HTTP and messaging have their advantages and disadvantages for microservices communication. HTTP is a more straightforward and well-established protocol, making it easier to implement and integrate with existing infrastructure. It also offers better compatibility with load balancers and proxies, making it a good choice for systems that require high availability and scalability. On the other hand, messaging provides a more robust and flexible communication mechanism for microservices. It allows for asynchronous and decoupled communication, which can be beneficial for systems that require loose coupling and event-driven architectures. Messaging also supports different patterns such as publish/subscribe, which can help to reduce complexity and improve scalability. Ultimately, the choice between HTTP and messaging will depend on the specific requirements of the microservices architecture. Teams should carefully consider factors, such as scalability, flexibility, and compatibility, when deciding on a communication protocol. In many cases, a hybrid approach that combines the strengths of HTTP and messaging may be the most effective solution.
In the era of mobile computing, app developers should be able to deploy actions quickly and make changes without redeploying the entire app. As a result, it has led to a new way of building software called "microservices." Microservices are small, self-contained parts of an app that each do their job and talk to each other through APIs. Even though these microservices are different, they work together to complete the job. Microservices are becoming an essential part of modern app architecture, so let's talk about what microservices architecture is, how they work, and why they are helpful. What Is Microservices Architecture? Microservices are also called microservice architecture. This is because large applications are broken up into smaller, easier-to-manage pieces by a process called "architecture." So, it gives us the tools to work and talk to each other independently. This method came about because monolithic architecture had problems that needed to be fixed. Since monoliths are big containers that hold all of an app's software parts, they are constrained, unreliable, rigid, and often built over time. Each part is set up independently with microservices, but they can communicate when necessary. As a result, developers can now create very complex software apps with ease, flexibility, and scalability. After discussing what "microservices architecture" means, let's move on to the next section. How Does Microservices Architecture Work? It focuses on grouping big and bulky apps. All microservices describe an app's specific parts and functions, like searching for data and keeping track of it. Also, these small services work together to make a single app. The client can send requests through the user interface. Also, the API gateway was made for one or more microservices that help do the task asked for. So, it's easy to solve even more significant, more complicated problems that require a set of microservices. Microservices systems let each part of a service run, grow, build, and deploy itself. This service and the others don't share any code or functions. Instead, messages move from one part of a program to another when clear APIs are used. Based on the problems it solves, each service in the system builds a different set of skills. When developers offer more code, some services split into smaller ones. It gives developers a lot of ways to solve problems, even if they don't know what those problems are yet. Let's take a look at how an online shopping channel works. It can think about different parts of the main app, like the customer database, product catalog, checkout options, and payment interfaces. These apps must be able to be deployed, built, and scaled independently. It would be best if you changed some things about the payment gateway interface. Then, after figuring out what needs to be changed, you can add that app to the tech stack. Since each service has its own code, you might wonder how they will talk to each other. API calls can be used to do this job well. Each service has an API endpoint connecting it to other stack apps. They talk to each other through HTTP requests. They can also use a message broker or find other communication methods that don't happen simultaneously. This tool sends a message from a user's account through a shared tunnel, which sorts the messages into different groups and sends them to additional services. Service mesh is a new method that is becoming more popular in Kubernetes. The main point is that these kinds of tasks can be written in different languages by different teams in other places. App developers can choose from different technical settings, development options, and programming languages. In-memory processing engines use several methods to improve speed and latency. Microservices use older design patterns like modularity, service-oriented architecture (SOA), and separation of concerns. Also, the extensive system they made is too complicated for a standard monolithic architecture to handle. Uses of Microservices Some of the most important uses of microservices are listed below. Data Processing Applications that use microservice architecture can handle more requests at once. This means that microservices can handle more data in less time. As a result, it lets programs run faster and better. Media Content Companies like Netflix and Amazon Prime Video handle billions of API requests daily. A microservices architecture will benefit OTT platforms that give users access to much media content. Microservices will ensure that all requests worldwide for different subdomains are quickly and correctly handled. Website Migration Migration of a website means making significant changes and rebuilding its most essential parts, like its structure, domain, and user interface. Microservices uses help you to eliminate downtime that hurts your business and ensure your migration plans go as planned. Transactions and Invoices Microservices uses are great for apps that handle many payments and transactions and send out bills. A business can lose a lot of money if an app can't take payments. Microservices use to improve an application's transaction features without changing anything else. Tools Used in Microservices It would help if you had a wide range of tools to help the architect with basic building tasks. Here is a list of some of these tools. 1. Operating system (OS) One of the most important things you need to know to make an app is how it runs. Linus is an operating system (OS) that gives users and developers much freedom. It can run program codes independently and has several security, storage, and networking options for large and small applications. 2. Programming Languages With a microservices architecture, you can focus on different programming languages for various app services. The tools and programming languages used to make an app depend on the microservice type. 3. Tools for Managing APIs and Testing Them With a microservices architecture, the different parts of an app must be able to talk to each other. Application programming interfaces are used to do this. For APIs to work right, they need to be managed, tested, and kept an eye on all the time. So, when making apps with a microservices architecture, tools for managing APIs and testing them are essential. 4. Toolkits In a microservices architecture, toolkits are tools used to develop an app. Developers can choose from many different toolkits, each one doing something different. Fabric8 and Seneca are two examples of microservices toolkits. 5. Messaging Tools It lets microservices talk with each other and the rest of the world. Apache Kafka and Rabbit MQ are messaging tools used as parts of the system by different small services. 6. Making Plans for Things Microservices architectural frameworks make it easy to build apps. They usually have a library of code and tools to set up and launch an app. 7. Tools to Keep Track of Things After you set up and run the microservices app, you need to monitor it to ensure everything goes as planned. Monitoring tools help developers to keep an eye on how an app works so they can find bugs and other problems before they happen. 8. Orchestration Tools A container is a group of code, executables, libraries, and files a microservice needs to run. Systems can use container orchestration with a microservices architecture to manage and optimize containers. 9. Tools That Don't Need Servers Serverless tools give the different microservices in an app even more freedom and mobility by eliminating the need for a server. It makes it easier to figure out how to divide and organize application tasks. Examples of Microservices The best tech companies use microservices to simplify their architecture, speed up development, make apps more responsive, and make updates easier. Here are three microservices examples of how market leaders have used them to improve their businesses in the real world. Let's delve right into what are examples of microservices architecture. Amazon Amazon works on many small things, like letting people place orders, sign in, make wish lists, and pay for things. Each piece is a small business app that does one job. Different business skills are used to build microservices. Each service is responsible for its data and how it organizes. A small group must work together to do a simple task for microservices to work. Jeff Bezos, the CEO of Amazon, devised the "two-pizza rule" to determine how big a microservices team should be. Two pizza teams run microservices at Amazon. The team couldn't have more people than two pizzas could feed. Amazon is building and running a flexible system with the help of microservices. Netflix In 2007, Netflix began letting people rent movies online. Scaling and service outages only took a year to become big problems. In 2008, Netflix went three days without sending DVDs to customers. It happened when they switched to a cloud system that was spread out and used Amazon Web Services as their cloud provider. After 2009, Netflix changed how its apps were built from a monolithic to a microservices architecture. The process was done in 2012. By switching to a microservices architecture, Netflix could fix its scaling problems and make its services available to more people worldwide. In 2015, Netflix's API gateway was linked to more than 500 microservices hosted in the cloud to handle 2 billion API requests daily. This made streaming cheaper, which helped Netflix to make more money. Uber Uber eliminated its one-piece structure because it slowed the company's growth, just like Amazon and Netflix. The ride-sharing platform had problems like being unable to develop and launch new features, fix bugs quickly, and connect its fast-growing global operations. It got to the point where the application architecture was so complicated that even small changes and updates needed developers with a lot of experience. Uber fixed the problems it caused by breaking its "monolithic" app into "microservices" that run in the cloud. Soon after, separate microservices were created for business tasks like the trip and the problems it caused with managing passengers. These services can talk to each other with the help of an API gateway. Using this microservices architecture, Uber solved some of its technical problems. This is why: Development teams took care of certain services, which helped make development projects better, faster, and easier to run. Teams put their attention on the services that needed to expand. It made adding more users to the app easy as demand multiplied. The Uber app could handle errors better, and some services can update without affecting the rest of the app. Features of Microservices Here are the most important features of microservices that you should know about it: 1. Split Up Into Different Pieces It is a significant feature of microservices, as the software is made up of many small services in a microservices architecture. Each service can be built, deployed, and updated separately without affecting the rest of the application. Instead of shutting down the app and starting it up again, you can change a few services to make it bigger. 2. Robust and Resistant to Failure A microservices architecture has another feature to make it less likely that an app will crash. Each service can fail, which can make things stop working. In a microservices environment, many different and unique services talk to each other to run operations, so failure is inevitable at some point. But if a microservices-based app is set up right, a function that isn't working should be able to reroute traffic away from itself so that its connected services can keep running. You can also quickly lower the risk of disruption by keeping an eye on microservices and bringing them back online as soon as possible if they go down. 3. Simple Routing Process Microservices are commonly used parts that can deal with both data and logic. "Dumb wires" connect these parts and send information from one to the next. Some enterprise applications develop the exact opposite of this simple routing method. For example, an enterprise service bus uses complicated systems to route messages, set up workflows, and apply business rules. On the other hand, microservices take requests, process them, and send the correct response back to the component that made the request. 4. Decentralized Operations Microservices use a wide range of platforms and technologies. Because of this, the traditional ways of running a microservices architecture from a central location don't work very well. With decentralized governance, microservices work better because developers worldwide make valuable tools for solving operational problems. You can even give these tools to other developers with the same issues. In the same way, a microservices architecture favors decentralized data management because each microservice application manages its database. On the other hand, monolithic systems tend to run all of their applications from a single logical database. 5. Built for Modern Businesses Microservices architecture was developed to meet the needs of 21st-century digital businesses. In traditional monolithic architectures, teams work together to build things like the user interface, technology layers, databases, and server-side logic. Microservices are made by people from different fields working together. Each team is responsible for making products that fit the needs of the service it is working on. A message bus is how these services send and receive information. Advantages of Using Microservices Developers and engineers can benefit from microservices architecture in a way that they can't get from monoliths. Here are some of the advantages of using microservices that stand out. Agility Microservices are used to make fixing bugs and adding new features more accessible. You can update the services without reorganizing the whole app, and if something goes wrong, you can roll back an update. In many older apps, finding a bug in one part can stop the whole process of putting the app on the market. New features are put on hold while a bug fix is added, tested, and publicized. Small and Focused Teams A microservice should be small enough that a single team can build, test, and use it. Smaller groups can move faster than larger ones. Large teams are usually less productive because it takes them longer to talk to each other, more work to run them, and they can't move as fast. Less Development Effort To make a program work better, smaller teams can work on different parts of it at the same time. It makes it much easier to find the most popular services, grow them without affecting the rest of the app, and improve the app. Small Code Base A single-piece app's code dependencies tend to get tangled up over time. The code must change in many places to add a new feature. A microservices architecture reduces the number of dependencies because it doesn't share data and code. It makes adding new features more accessible. Multiple Technologies With the help of a "tech stack," teams can figure out which technologies work best for their service. Fault Isolation If a microservice goes down, the whole app won't be affected as long as other microservices handle errors before it. For example, you can use the Circuit Breaker pattern or devise your solution. Because of this, the microservices talk to each other using asynchronous messaging configurations. Scalability Services could grow independently, so you could give more resources to subsystems that needed them without giving more to the whole application. You could run more services on a single host using an orchestrator like Service Fabric or Kubernetes. Data Isolation Schema changes are much easier to make now that they only affect one microservice. If different parts of a monolithic app show the same information, it could be hard to update the schema. It makes it risky to change a schema. Flexibility Since each service works independently, each microservice can use a different programming language. But it's best not to use more than one modern programming language as much as possible. Faster Deployment Not only are microservices easier for most developers to understand, but they also take less time to set up. If you change just one thing in the code for a monolithic structure, it changes everything. You can use microservices, so you don't need to change how other services work. Independent Deployment In an app, each microservice must be a full stack. It gives microservices the freedom to grow whenever they want. Microservices are small, so development teams can work on a single microservice, fix any bugs, and then redeploy it without redeploying the whole application. Because microservice architecture is flexible, you don't need a law from Congress to add or change a line of code or add or take away features. The software promises to improve business structures by making them more stable and separating problems. Conclusion With a microservices architecture, developers can solve several significant problems that were hard to solve with single-piece solutions in the past. But both too much development and too much time spent on guard can be problems on their own. Because of this, a microservices architecture needs a platform for observability. With observability, engineers and developers can get the flexibility and reconfigurability of microservices.
This is an article from DZone's 2023 Software Integration Trend Report.For more: Read the Report A microservices architecture is an established pattern for building a complex system that consists of loosely coupled modules. It is one of the most talked-about software architecture trends in the last few years. It seems to be a surprisingly simple idea to break a large, interdependent system into many small, lightweight modules that can make software management easier. Here's the catch: After you have broken down your monolith application into small modules, how are you supposed to connect them together in a meaningful way? Unfortunately, there is no single right answer to this question, but as is so often the case, there are a few approaches that depend on the application and the individual use case. Two common protocols used in microservices are HTTP request/response with resource APIs and lightweight asynchronous messaging when communicating updates across several microservices. Let's explore these protocols. Types of Communication Microservices can communicate through many different modes of communication, each one targeting a different use case. These types of communications can be primarily classified in two dimensions. The first dimension defines if the communication protocol is synchronous or asynchronous: SYNCHRONOUS vs. ASYNCHRONOUS COMMUNICATION Synchronous Asynchronous Communication pattern The client sends a request and waits for a response from the server. Communication is not in sync, which means it does not happen in real time. Protocols HTTP/HTTPS AMQP, MQTT Coupling The client code can only continue its task further when it receives the server response. In the context of distributed messaging, coupling implies that request processing will occur at an arbitrary point in time. Failure isolation It requires the downstream server to be available or the request fails. If the consumer fails, the sender can still send messages. The messages will be picked up when the consumer recovers. Table 1 The second dimension defines if the communication has a single receiver or multiple receivers: COMMUNICATION VIA SINGLE vs. MULTIPLE RECEIVERS Single Receiver Multiple Receivers Communication pattern It implies that there is point-to-point communication that delivers a message to exactly one consumer that is reading from the channel, and that the message is processed only once. Communication from the sender is available to multiple receivers. Example It is well-suited for sending asynchronous commands from one microservice to another. The publish/subscribe mechanism is where a publisher publishes a message to a channel and the channel can be subscribed by multiple subscribers/receivers to receive the message asynchronously. Table 2 The most common type of communication between microservices is single-receiver communication with a synchronous protocol like HTTP/HTTPS when invoking a REST API. Microservices typically use messaging protocols for asynchronous communication between microservices. This asynchronous communication may involve a single receiver or multiple receivers depending on the application's needs. Representational State Transfer Representational state transfer (REST) is a popular architectural style for request and response communication, and it can serve as a good example for the synchronous communication type. This is based on the HTTP protocol, embracing verbs such as GET, POST, PUT, DELETE, etc. In this communication pattern, the caller waits for a response from the server. Figure 1: REST API-based communication REST is the most commonly used architectural style for communication between services, but heavy reliance on this type of communication has some negative consequences when it comes to a microservices architecture: Multiple round trips (latency) – The client often needs to execute multiple trips to the server to fetch all the data the client requires. Each endpoint specifies a fixed amount of data, and in many cases, that data is only a subset of what a client needs to populate their page. Blocking – When invoking a REST API, the client is blocked and is waiting for a server response. This may hurt application performance if the application thread is processing other concurrent requests. Tight coupling – The client and server need to know about each other. It increases complexity over time and reduces portability. Messaging Messaging is widely used in a microservices architecture, which follows the asynchronous protocol. In this pattern, a service sends a message without waiting for a response, and one or more services process the message asynchronously. Asynchronous messaging provides many benefits but also brings challenges such as idempotency, message ordering, poison message handling, and complexity of message broker, which must be highly available. It is important to note the difference between asynchronous I/O and the asynchronous protocol. Asynchronous I/O means that the calling thread is not blocked while the I/O operations are executed. This is an implementation detail in terms of the software design. The asynchronous protocol means the sender does not wait for a response. Figure 2: Messaging-based communication Asynchronous messaging has some advantages over synchronous messaging: Loose coupling – The message producer does not need to know about the consumer(s). Multiple subscribers – Using a publisher/subscriber (pub/sub) model, multiple consumers can subscribe to receive events. Resiliency or failure isolation – If the consumer fails, the producer can still send messages. The messages will be picked up when the consumer recovers from failure. This is especially useful in a microservices architecture because each microservice has its own lifecycle. Non-blocking – The producers and consumers can send and process messages at their own pace. Though asynchronous messaging has many advantages, it comes with some tradeoffs: Tight coupling with the messaging infrastructure – Using a particular vendor/messaging infrastructure may cause tight coupling with that infrastructure. It may become difficult to switch to another vendor/messaging infrastructure later. Complexity – Handling asynchronous messaging may not be as easy as designing a REST API. Duplicate messages must be handled by de-duplicating or making the operations idempotent. It is hard to implement request-response semantics using asynchronous messaging. To send a response, another queue and a way to correlate request and response messages are both needed. Debugging can also be difficult as it is hard to identify which request in Service A caused the wrong behavior in Service B. Asynchronous messaging has matured into a number of messaging patterns. These patterns apply to scenarios when several parts of a distributed system must communicate with one another in a dependable and scalable way. Let's take a look at some of these patterns. Pub/Sub Pattern The pub/sub pattern implies that a publisher sends a message to a channel on a message broker. One or more subscribers subscribe to the channel and receive messages from the channel in an asynchronous manner. This pattern is useful when a microservice needs to broadcast information to a significant number of consumers. Figure 3: Pub/sub pattern The pub/sub pattern has the following advantages: It decouples publishers and subscribers that need to communicate. Publishers and subscribers can be managed independently, and messages can be managed even if one or more subscribers are offline. It increases scalability and improves responsiveness of the publisher. The publisher can quickly publish a message to the input channel, then return to its core processing responsibilities. The messaging infrastructure is responsible for ensuring messages are delivered to interested subscribers. It provides separation of concerns for microservices. Each microservice can focus on its core responsibilities, while the message broker handles everything required to reliably route messages to multiple subscribers. There are a few disadvantages of using this pattern: The pub/sub pattern introduces high semantic coupling in the messages passed by the publishers to the subscribers. Once the structure of the data is established, it is often difficult to change. To change the message structure, all subscribers must be altered to accept the changed format. This can be difficult or impossible if the subscribers are external. Another drawback of the pub/sub pattern is that it is difficult to gauge the health of subscribers. The publisher does not have knowledge of the health status of the systems listening to the messages. As a pub/sub system scales, the broker often becomes a bottleneck for message flow. Load surges can slow down the pub/sub system, and subscribers can get a spike in response time. Queue-Based Pattern In the queue-based pattern, a sender posts a message to a queue containing the data required by the receiver. The queue acts as a buffer, storing the message until it is retrieved by the receiver. The receiver retrieves messages from the queue and processes them at its own pace. This pattern is useful for any application that uses services that are subject to overloading. Figure 4: Queue-based pattern The queue-based pattern has the following advantages: It can help maximize scalability because both the number of queues and the number of services can be scaled to meet demand. It can help maximize availability. Delays arising in the producer or consumer won't have an immediate or direct impact on the services, which can continue to post messages to the queue even when the consumer isn't available or is under heavy load to process messages. There are some disadvantages of using this pattern: When a consumer receives a message from the queue, the message is no longer available in the queue. If a consumer fails to process the message, the message is lost and may need a rollback in the consumer. Message queues do not come out of the box. We need to create, configure, and monitor them. It can cause operational complexity when systems are scaled up. Keys To Streamlined Messaging Infrastructure Asynchronous communication is usually managed through a message broker. There are some factors to consider when choosing the right messaging infrastructure for asynchronous communication: Scalability – the ability to scale automatically when there is a load surge on the message broker Data persistency – the ability to recover messages in case of reboot/failure Consumer capability – whether the broker can manage one-to-one and/or one-to-many consumers Monitoring – whether monitoring capabilities are available Push and pull queue – the ability to handle push and pull delivery by message queues Security – proper authentication and authorization for messaging queues and topics Automatic failover – the ability to connect to a failover broker automatically when one broker fails without impacting publisher/consumer Conclusion More and more, microservices are becoming the de facto approach for designing scalable and resilient systems. There is no single approach for all communications between microservices. While RESTful APIs provide a request-response model to communicate between services, asynchronous messaging offers a more scalable producer-consumer relationship between different services. And although microservices can communicate with each other via both messaging and REST APIs, messaging architectures are ideal for improving agility and moving quickly. They are commonly found in modern applications that use microservices or any application that has decoupled components. When it comes to choosing a right style of communication for your microservices, be sure to match the needs of the consumer with one or more communication types to offer a robust interface for your services. This is an article from DZone's 2023 Software Integration Trend Report.For more: Read the Report
What Is Distributed Tracing? The rise of microservices has enabled users to create distributed applications that consist of modular services rather than a single functional unit. This modularity makes testing and deployment easier while preventing a single point of failure with the application. While applications begin to scale and distribute their resources amongst multiple cloud-native services, tracing a single transaction becomes tedious and nearly impossible. Hence, developers need to apply distributed tracing techniques. Distributed tracing allows a single transaction to be tracked across the front end to the backend services while providing visibility into the systems’ behavior. How Distributed Tracing Works The distributed tracing process operates on a fundamental concept of being able to trace every transaction through multiple distributed components of the application. To achieve this visibility, distributed tracing technology uses unique identifiers, namely the Trace ID, to tag each transaction. The system then puts together each trace from the various components of the application by using this unique identifier, thus building a timeline of the transaction. Each trace consists of one or more spans that represent a single operation within a single trace. It is essential to understand that a span can be referred to as a parent span for another span, indicating that the parent span triggers the child span. Implementing Distributed Tracing Setting up a distributed tracing depends on the selected solution. However, every solution will consist of these common steps. These three steps ensure developers have a solid base to start their distributed tracing journey: Setting up a distributed tracing system. Instrumenting code for tracing. Collecting and storing trace data. 1. Setting Up a Distributed System Selecting the right distributed tracing solution is crucial. Key aspects, such as compatibility, scale, and other important factors must be addressed. Many distributed tracing tools support various programming languages, including Node.js, Python, Go, .NET, Java, etc. These tools allow developers to use a single solution for distributed tracing across multiple services. 2. Instrumenting Code for Tracing Depending on the solution, the method of integration may change. The most common approach many solutions provide is using an SDK that collects the data during runtime. For example, developers using Helios with Node.js require installing the latest Helios OpenTelemetry SDK by running the following command: npm install --save helios-opentelemetry-sdk Afterward, the solution requires defining the following environment variables. Finally, it enables the SDK to collect the necessary data from the service: export NODE_OPTIONS="--require helios-opentelemetry-sdk" export HS_TOKEN="{{HELIOS_API_TOKEN}" export HS_SERVICE_NAME="<Lambda01>" export HS_ENVIRONMENT="<ServiceEnvironment01>" 3. Collecting and Storing Trace Data In most distributed tracing systems, trace data collection occurs automatically during the runtime. Then, this data makes its way to the distributed tracing solution, where the analysis and visualization occur. The collection and storage of the trace data depend on the solution in use. For example, if the solution is SaaS-based, the solution provider will take care of all trace data collecting and storage aspects. However, if the tracing solution is self-hosted, the responsibility of taking care of these aspects falls on the administrators of the solution. Analyzing Trace Data Analyzing trace data can be tedious. However, visualizing the trace data makes it easier for developers to understand the actual transaction flow and identify anomalies or bottlenecks. The following demonstrates the flow of the transaction through the various services and components of the application. An advanced distributed tracing system may highlight errors and bottlenecks that each transaction runs through. Since the trace data contains the time it takes for each service to process the transaction, developers can analyze the latencies and identify abnormalities that may impact the application’s performance. Identifying an issue using the distributed tracing solution can provide insight into the problem that has taken place. However, to gain further details regarding the issue, developers may need to use additional tools that provide added insight with observability or the capability to correlate traces with the logs to identify the cause. Distributed tracing solutions, such as Helios, offer insight into the error’s details, which eases the developer’s burden. Best Practices for Distributed Tracing A comprehensive distributed tracing solution empowers developers to respond to crucial issues swiftly. The following best practices set the fundamentals for a successful distributed tracing solution. 1. Ensuring Trace Data Accuracy and Completeness Collecting trace data from services enable developers to identify the performance and latency of all the services each transaction flows through. However, when the trace data does not contain information from a specific service, it reduces the accuracy of the entire trace and its overall completeness. To ensure developers obtain the most out of distributed tracing, it is vital that the system collects accurate trace information from all services to reflect the original data. 2. Balancing Trace Overhead and Detail Collecting all trace information from all the services will provide the most comprehensive trace. However, collecting most trace information comes at the cost of the overhead to the overall application or the individual service. The tradeoff between the amount of data collected and the acceptable overhead is crucial. Planning for this tradeoff ensures distributed tracing does not harm the overall solution, thus outweighing the benefits the solution brings. Another take on balancing these aspects is filtering and sampling the trace information to collect what is required. However, this would require additional planning and a thorough understanding of the requirement to collect valuable trace information. 3. Protecting Sensitive Data in Trace Data Collecting trace information from transactions includes collecting payloads of the actual transaction. This information is usually considered sensitive since it may contain personally identifiable information of customers, such as driver’s license numbers or banking information. Regulations worldwide clearly define what information to store during business operations and how to handle this information. Therefore, it is of unparalleled importance that the information collected must undergo data obfuscation. Helios enables its users to easily obfuscate sensitive data from the payloads collected, thereby enabling compliance with regulations. In addition to obfuscation, Helios provides other techniques to enhance and filter out the data sent to the Helios platform. Distributed Tracing Tools Today, numerous distributed tracing tools are available for developers to easily leverage their capabilities in resolving issues quicker. 1. Lightstep Lightstep is a cloud-agnostic distributed tracing tool that provides full-context distributed tracing across multi-cloud environments or microservices. It enables developers to integrate the solution with complex systems with little extra effort. It also provides a free plan with the features required for developers to get started on their distributed tracing journey. In addition, the free plan offers many helpful features, including data ingestion, analysis, and monitoring. Source: LightStep UI 2. Zipkin Zipkin is an open-source solution that provides distributed tracing with easy-to-use steps to get started. It enhances its distributed tracing efforts by enabling the integration with Elasticsearch for efficient log searching. Source: Zipkin UI It was developed at Twitter to gather crucial timing data needed to troubleshoot latency issues in service architectures, and it is straightforward to set up with a simple Docker command: docker run -d -p 9411:9411 openzipkin/zipkin 3. Jaeger Tracing Jaeger Tracing is yet another open-source solution that provides end-to-end distributed tracing and the ability to perform root cause analysis to identify performance issues or bottlenecks across each trace. It also supports Elasticsearch for data persistence and exposes Prometheus metrics by default to help developers derive meaningful insights. In addition, it allows filtering traces based on duration, service, and tags using the pre-built Jaeger UI. Source: Jaeger Tracing 4. SigNoz SigNoz is an open-source tool that enables developers to perform distributed tracing across microservices-based systems while capturing logs, traces, and metrics and later visualizing them within its unified UI. It also provides insightful performance metrics such as the p50, p95, and p99 latency. Some key benefits of using SigNoz include the consolidated UI that showcases logs, metrics, and traces while supporting OpenTelemetry. Source: SigNoz UI 5. New Relic New Relic is a distributed tracing solution that can observe 100% of an application’s traces. It provides compatibility with a vast technology stack and support for industry-standard frameworks such as OpenTelemetry. It also supports alerts to diagnose errors before they become major issues. New Relic has the advantage of being a fully managed cloud-native with support for on-demand scalability. In addition, developers can use a single agent to automatically instrument the entire application code. Source: New Relic UI 6. Datadog Datadog is a well-recognized solution that offers cloud monitoring as a service. It provides distributed tracing capabilities with Datadog APM, including additional features to correlate distributed tracing, browser sessions, logs, profiles, network, processes, and infrastructure metrics. In addition, Datadog APM allows developers to easily integrate the solution with the application. Developers can also use the solution’s capabilities to seamlessly instrument application code to monitor cloud infrastructure. Source: DataDog UI 7. Splunk Splunk offers a distributed tracing tool capable of ingesting all application data while enabling an AI-driven service to identify error-prone microservices. It also adds the advantage of correlating between application and infrastructure metrics to better understand the fault at hand. You can start with a free tier that brings in essential features. However, it is crucial to understand that this solution will store data in the cloud; this may cause compliance issues in some industries. Source: Splunk UI 8. Honeycomb Honeycomb brings in distributed tracing capabilities in addition to its native observability functionalities. One of its standout features is that it uses anomaly detection to pinpoint which spans are tied to bad user experiences. It supports OpenTelemetry to enable developers to instrument code without being stuck to a single vendor while offering a pay-as-you-go pricing model to only pay for what you use. Source: HoneyComb UI 9. Helios Helios brings advanced distributed tracing techniques that enhance the developer’s ability to get actionable insight into the end-to-end application flow by adapting OpenTelemetry’s context propagation framework. The solution provides visibility into your system across microservices, serverless functions, databases, and third-party APIs, thus enabling you to quickly identify, reproduce, and resolve issues. Source: Helios Sandbox Furthermore, Helios provides a free trace visualization tool based on OpenTelemetry that allows developers to visualize and analyze a trace file by simply uploading it. Conclusion Distributed tracing has seen many iterations and feature enhancements that allow developers to easily identify issues within the application. It reduces the time taken to detect and respond to performance issues and helps understand the relationships between individual microservices. The future of distributed tracing would incorporate multi-cloud tracing, enabling developers to troubleshoot issues across various cloud platforms. Also, these platforms consolidate the trace, thus cutting off the requirement for developers to trace these transactions across each cloud platform manually, which is time-consuming and nearly impossible to achieve. I hope you have found this helpful. Thank you for reading!
In this article, we’re going to compare some essential metrics of web applications using two different Java stacks: Spring Boot and Eclipse MicroProfile. More precisely, we’ll implement the same web application in Spring Boot 3.0.2 and Eclipse MicroProfile 4.2. These releases are the most recent at the time of this writing. Since there are several implementations of Eclipse MicroProfile, we’ll be using one of the most famous: Quarkus. At the time of this writing, the most recent Quarkus release is 2.16.2. This mention is important regarding Eclipse MicroProfile because, as opposed to Spring Boot, which isn’t based on any specification and, consequently, the question of the implementation doesn’t exist, Eclipse MicroProfile has largely been adopted by many editors who provide different implementations, among which Quarkus, Wildfly, Open Liberty and Payara are from the most evangelical. In this article, we will implement the same web application using two different technologies, Spring Boot and Quarkus, such that to compare their respective two essential metrics: RSS (Resident Set Size) and TFR (Time to First Request). The Use Case The use case that we’ve chosen for the web application to be implemented is a quite standard one: the one of a microservice responsible to manage press releases. A press release is an official statement delivered to members of the news media for the purpose of providing information, creating an official statement, or making a public announcement. In our simplified case, a press release consists in a set of data like a unique name describing its subject, an author, and a publisher. The microservice used to manage press releases is very straightforward. As with any microservice, it exposes a REST API allowing for CRUD press releases. All the required layers, like domain, model, entities, DTOs, mapping, persistence, and service, are present as well. Our point here is not to discuss the microservices structure and modus operandi but to propose a common use case to be implemented in the two similar technologies, Spring Boot and Quarkus, to be able to compare their respective performances through the mentioned metrics. Resident Set Size (RSS) RSS is the amount of RAM occupied by a process and consists of the sum of the following JVM spaces: Heap space Class metadata Thread stacks Compiled code Garbage collection RSS is a very accurate metric, and comparing applications based on it is a very reliable way to measure their associated performances and footprints. Time to First Request (TFR) There is a common concern about measuring and comparing applications' startup times. However, logging it, which is how this is generally done, isn’t enough. The time you’re seeing in your log file as being the application startup time isn’t accurate because it represents the time your application or web server started, but not the one required that your application starts to receive requests. Application and web servers, or servlet containers, might start in a couple of milliseconds, but this doesn’t mean your application can process requests. These platforms often delay work through the process and may give a false, lazy initialization indication about the TFR. Hence, to accurately determine the TFR, in this report, we’re using Clément Escofier’s script time.js, found here in the GitHub repository, which illustrates the excellent book Reactive Systems in Java by Clément Escoffier and Ken Finnigan. Spring Boot Implementation To compare the metrics presented above for the two implementations, you need to clone and run the two projects. Here are the steps required to experience the Spring Boot implementation: Shell $ git clone https://github.com/nicolasduminil/Comparing-Resident-Size- Set-Between-Spring-Boot-and-Quarkus.git metrics $ cd metrics $ git checkout spring-boot $ mvn package $ java -jar target/metrics.jar Here you start by cloning the GIT repository, and once this operation is finished, you go into the project’s root directory and do a Maven build. Then you start the Spring Boot application by running the über JAR created by the spring-boot-maven-plugin. Now you can test the application via its exposed Swagger UI interface by going here. Please take a moment to use the feature that tries it out that Swagger UI offers. The order of operations is as follows: First, the POST endpoint is to create a press release. Please use the editor to modify the JSON payload proposed by default. While doing this, you should leave the field pressReleaseId having a value of “0” as this is the primary key that will be generated by the insert operation. Below, you can see an example of how to customize this payload: JSON { "pressReleaseId": 0, "name": "AWS Lambda", "author": "Nicolas DUMINIL", "publisher": "ENI" } Next, a GET /all is followed by a GET /id to check that the previous operation has successfully created a press release. A PUT to modify the current press release A DELETE /id to clean-up Note: Since the ID is automatically generated by a sequence, as explained, the first record will have the value of “1.” You can use this value in GET /id and DELETE /id requests. Notice that the press release name must be unique. Now, once you have experienced your microservice, let’s see its associated RSS. Proceed as follows: Shell $ ps aux | grep metrics nicolas 31598 3.5 1.8 13035944 598940 pts/1 Sl+ 19:03 0:21 java -jar target/metrics.jar nicolas 31771 0.0 0.0 9040 660 pts/2 S+ 19:13 0:00 grep --color=auto metrics $ ps -o pid,rss,command -p 31598 PID RSS COMMAND 31598 639380 java -jar target/metrics.ja Here, we get the PID of our microservice by looking up its name, and once we have it, we can display its associated RSS. Notice that the command ps -o above will display the PID, the RSS, and the starting command associated with the process, which PID is passed as the -p argument. And as you may see, the RSS for our process is 624 MB (639380 KB). If you’re hesitating about how to calculate this value, you can use the following command: Shell $ echo 639380/1024 | bc 624 As for the TFR, all you need to do is to run the script time.js, as follows: Shell node time.js "java -jar target/metrics.jar" "http://localhost:8080/" 173 ms To resume, our Spring Boot microservice has a RSS of 624 MB and a TFR of 173 ms. Quarkus Implementation We need to perform these same operations to experience our Quarkus microservice. Here are the required operations: Shell $ git checkout quarkus $ mvn package quarkus:dev Once our Quarkus microservice has started, you may use the Swager UI interface here. And if you’re too tired to use the graphical interface, then you may use the curl scripts provided in the repository ( post.sh, get.sh, etc.) as shown below: Shell java -jar target/quarkus-ap/quarkus-run.jar & ./post.sh ./get.sh ./get-1.sh 1 ./update.sh ... Now, let’s see how we do concerning our RSS and TFR: Shell $ ps aux | grep quarkus-run nicolas 24776 20.2 0.6 13808088 205004 pts/3 Sl+ 16:27 0:04 java -jar target/quarkus-app/quarkus-run.jar nicolas 24840 0.0 0.0 9040 728 pts/5 S+ 16:28 0:00 grep --color=auto quarkus-run $ ps -o pid,rss,command -p 24776 PID RSS COMMAND 24776 175480 java -jar target/quarkus-app/quarkus-run.jar $ echo 175480/1024 | bc 168 $ node time.js "java -jar target/quarkus-app/quarkus-run.jar" "http://localhost:8081/q/swagger-ui" 121 ms As you can see, our Quarkus microservice uses an RSS of 168MB, i.e., almost 500MB less than the 624MB with Spring Boot. Also, the TFR is slightly inferior (121ms vs. 173ms). Conclusion Our exercise has compared the RSS and TFR metrics for the two microservices executed with the HotSpot JVM (Oracle JDK 17). Spring Boot and Quarkus support the compilation into native executables through GraalVM. It would have been interesting to compare these same metrics of the native replica of the two microservices, and if we didn’t do it here, that’s because Spring Boot heavily relies on Java introspection and, consequently, it’s significantly more difficult to generate Spring Boot native microservices than Quarkus ones. But stay tuned; it will come soon. The source code may be found here. The GIT repository has a master branch and two specific ones, labeled spring-boot and, respectively, quarkus. Enjoy!
As I talked about in my other article about some of the top Java REST API frameworks, in today's world of modern software development, REST API frameworks play a crucial role in developing efficient and scalable microservices. Java has several frameworks for developing REST APIs, but three of the most popular ones are Spring Boot, Quarkus, and Micronaut. In this article, we will compare these frameworks, their features, and their pros and cons. Spring Boot Spring Boot is a popular Java-based framework that is widely used for developing RESTful APIs. It is built on top of the Spring Framework and provides a simplified and opinionated approach to building microservices. Spring Boot provides a lot of built-in features and functionalities that make it easy to get started with microservices. Pros Easy to learn and get started with Large community support and active development Provides a lot of built-in features and functionalities Good support for testing, security, and database integration Good support for building and deploying containerized applications Cons Can be heavy and slow, especially for smaller applications Configuration can be complex and verbose Some features are opinionated, which can limit flexibility Can require a lot of boilerplate code Quarkus Quarkus is a relatively new Java-based framework that is designed specifically for developing lightweight and fast microservices. It is built on top of popular libraries like Hibernate, Eclipse MicroProfile, and Vert.x, and provides a container-first approach to building microservices. Pros Extremely fast startup time and low memory footprint Good support for building containerized applications Provides a lot of built-in features and functionalities Good support for testing, security, and database integration Good support for building reactive applications Cons Limited community support compared to Spring Boot Fewer features and functionalities than Spring Boot Requires a different mindset and approach to development Limited support for legacy applications and frameworks Micronaut Micronaut is another Java-based framework that is designed for building lightweight and modular microservices. It is built on top of popular libraries like Netty, RxJava, and Groovy, and provides a lot of built-in features and functionalities that make it easy to get started with microservices. Pros Extremely fast startup time and low memory footprint Good support for building containerized applications Provides a lot of built-in features and functionalities Good support for testing, security, and database integration Good support for building reactive applications Cons Limited community support compared to Spring Boot Fewer features and functionalities than Spring Boot Requires a different mindset and approach to development Limited support for legacy applications and frameworks Basic Code Examples Here are some basic code examples for each framework: Spring Boot To create a RESTful web service using Spring Boot, you can start with the following code example: kotlin @RestController public class GreetingController { @GetMapping("/hello") public String hello() { return "Hello, World!"; } } This code defines a REST endpoint at /hello that returns a string "Hello, World!" when accessed. Quarkus To create a RESTful web service using Quarkus, you can start with the following code example: less @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello, World!"; } } This code defines a REST endpoint at /hello that returns a string "Hello, World!" when accessed. Micronaut To create a RESTful web service using Micronaut, you can start with the following code example: less @Controller("/hello") public class GreetingController { @Get @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello, World!"; } } This code defines a REST endpoint at /hello that returns a string "Hello, World!" when accessed. Note that these code examples are very basic and don't demonstrate the full capabilities of each framework. Each framework has its own set of features and functionalities that make it unique and suitable for different use cases. Performance In terms of performance, all three frameworks have their own strengths and weaknesses. Here's a brief comparison: Spring Boot Spring Boot is a mature and widely-used framework, with a large and active community. It has been optimized for performance over the years and provides a lot of configuration options to fine-tune performance. However, Spring Boot can be memory-intensive, especially when dealing with large applications or running on limited resources. Quarkus Quarkus is a relatively new framework that is designed to be fast and lightweight. It has been optimized for low memory usage and fast startup times, making it ideal for containerized environments such as Kubernetes. Quarkus is based on a reactive programming model, which can help improve performance by allowing for better utilization of resources. Micronaut Micronaut is also a lightweight and fast framework, designed to be used in microservices architectures. It uses compile-time DI and AOP to minimize runtime overhead, which can help improve performance. Micronaut also has a low memory footprint and fast startup times, making it ideal for serverless architectures. Conclusion In conclusion, all three frameworks - Spring Boot, Quarkus, and Micronaut - have their own strengths and weaknesses. Spring Boot is the most popular and feature-rich framework, but it can be heavy and slow. Quarkus and Micronaut are designed specifically for developing lightweight and fast microservices, but they may have limited community support and features. The choice of framework ultimately depends on the specific requirements of your project and your personal preferences. Overall, each framework has its own strengths and can perform well under different circumstances. It's important to choose the right framework for your use case, based on factors such as the size of your application, the resources you have available, and your performance requirements.
When applying hexagonal architecture (ports and adapters) with access to infrastructure elements like databases is done by the mean of adapters, which are just implementations of interfaces (ports) defined by the domain. In this article, we are going to provide two implementations of the same repository port, one in-memory and another based on JPA, focusing on how to test both implementations with the same set of tests. Context Many software solutions usually developed in the enterprise context have some state that needs to be persisted in a durable store for later access. Depending on the specific functional and non-functional requirements, selecting the correct persistence solution can be hard to make and most likely require an Architecture Decision Record (ADR) where the rationale of the selection, including alternatives and tradeoffs, is detailed. For persisting your application state, most likely, you will look at the CAP Theorem to make the most adequate decision. This decision process should not delay the design and development of your application’s domain model. Engineering teams should focus on delivering (business) value, not on maintaining a bunch of DDL scripts and evolving a highly changing database schema, for some weeks (or months) later, realize it would have been better to use a document database instead of a relational database. Also, focusing on delivery domain value prevents the team from taking a domain-related decision based on the constraints of a too-early-taken technical and/or infrastructure-related decision (i.e., the database technology in this case). As Uncle Bob said in this tweet, the architecture shall allow deferring the framework decisions (and infrastructure ones). Deferring Infrastructure-Related Decisions Coming back to the database technology example, a way of deferring the infrastructure decision regarding which database technology shall be used, would be starting with a simple in-memory implementation of your repository where the domain entities can be stored in a list in memory. This approach accelerates the discovery, design, and implementation of features and domain use cases, enabling fast feedback cycles with your stakeholders about what matters: Domain Value. Now, you might be thinking, “but then, I’m not delivering an e2e working feature,” or “how do I verify the feature with an in-memory adapter of my repository?” Here, architecture patterns like Hexagonal Architecture (also known as ports and adapters) and methodologies like DDD (not mandatory for having a clean architecture and ultimately clean code) come into action. Hexagonal Architecture Many applications are designed following the classical three-layered architecture: Presentation/controller Service (business logic) Persistence layers This architecture tends to mix domain definition (e.g., domain entities and value objects) with tables (e.g., ORM entities), usually represented as simple Data Transfer Objects. This is shown below: On the contrary, with hexagonal architecture, the actual persistence related classes are all defined based on the domain model. By using the port (interface) of the repository (which is defined as part of the domain model), it is possible to define integration test definitions agnostic of the underlying technology, which verifies the domain expectations towards the repository. Let’s see what this looks like in code in a simple domain model for managing students. Show Me the Code So how does this repository port look as part of the domain? It essentially defines the expectations of the domain towards the repository, having all the methods defined in terms of domain ubiquitous language: Java public interface StudentRepository { Student save(Student student); Optional<Student> retrieveStudentWithEmail(ContactInfo contactInfo); Publisher<Student> saveReactive(Student student); } Based on the repository port specification, it is possible to create the integration test definition, which is only dependent on the port, and agnostic of any underlying technology decision made for persisting the domain state. This test class will have a property as an instance of the repository interface (port) over which the expectations are verified. The next image shows what these tests look like: Java public class StudentRepositoryTest { StudentRepository studentRepository; @Test public void shouldCreateStudent() { Student expected = randomNewStudent(); Student actual = studentRepository.save(expected); assertAll("Create Student", () -> assertEquals(0L, actual.getVersion()), () -> assertEquals(expected.getStudentName(), actual.getStudentName()), () -> assertNotNull(actual.getStudentId()) ); } @Test public void shouldUpdateExistingStudent() { Student expected = randomExistingStudent(); Student actual = studentRepository.save(expected); assertAll("Update Student", () -> assertEquals(expected.getVersion()+1, actual.getVersion()), () -> assertEquals(expected.getStudentName(), actual.getStudentName()), () -> assertEquals(expected.getStudentId(), actual.getStudentId()) ); } } Once the repository test definition is completed, we can create a test runtime (integration test) for the in-memory repository: Java public class StudentRepositoryInMemoryIT extends StudentRepositoryTest { @BeforeEach public void setup() { super.studentRepository = new StudentRepositoryInMemory(); } } Or a bit more elaborated integration test for JPA with Postgres: Java @Testcontainers @ContextConfiguration(classes = {PersistenceConfig.class}) @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class StudentRepositoryJpaIT extends StudentRepositoryTest{ @Autowired public StudentRepository studentRepository; @Container public static PostgreSQLContainer container = new PostgreSQLContainer("postgres:latest") .withDatabaseName("students_db") .withUsername("sa") .withPassword("sa"); @DynamicPropertySource public static void overrideProperties(DynamicPropertyRegistry registry){ registry.add("spring.datasource.url", container::getJdbcUrl); registry.add("spring.datasource.username", container::getUsername); registry.add("spring.datasource.password", container::getPassword); registry.add("spring.datasource.driver-class-name", container::getDriverClassName); } @BeforeEach public void setup() { super.studentRepository = studentRepository; } } Both test runtimes are extending the same test definition, so we can be sure that, when switching from the in-memory adapter to the final JPA-full-featured persistence, no test shall be affected because it’s only needed to configure the corresponding test runtime. This approach will allow us to define the tests of the repository port without any dependency on frameworks and reuse those tests once the domain is better defined, being more stable, and the team decides to move forward with the database technology that better fulfills the solution quality attributes. The overall structure of the project is shown in the next image: Where: student-domain: Module with the domain definition, including entities, value objects, domain events, ports, etc. This module has no dependencies with frameworks, being as pure Java as possible. student-application: Currently, this module has no code since it was out-of-scope of the article. Following hexagonal architecture, this module orchestrates invocations to the domain model, being the entry point of the domain use cases. Future articles will enter more details. student-repository-test: This module contains the repository test definitions, with no dependencies on frameworks, and only verifies the expectation of the provided repository port. student-repository-inmemory: In-memory implementation of the repository port defined by the domain. It also contains the integration test, which provides the in-memory adapter of the port to the test definition of the student-repository-test. student-repository-jpa: JPA implementation of the repository port defined by the domain. It also contains the integration test, which provides the in-memory adapter of the port to the test definition of the student-repository-test. This integration test setup is a bit more complex since it spin-up a basic Spring context together with a Postgres container. student-shared-kernel: This module is out-of-scope of the article; it provides some utility classes and interfaces for designing the rest of the project. Conclusion Using this architectural style for your projects promotes good separation between the domain model and infrastructure elements around it, ensuring the latter will not influence the former while promoting good code quality (clean code) and high maintainability. The code of this article can be found in my personal GitHub repository.
Microservice architecture is a popular software design pattern that involves dividing a large application into smaller, independent services that can be developed, deployed, and scaled autonomously. Each microservice is responsible for a specific function within the larger application and communicates with other services via well-defined interfaces, often leveraging lightweight communication protocols such as REST or message queues. Advantages of Microservice Architecture Microservice architecture provides several advantages. Here are some benefits of using microservices: Maintainability: Smaller services are more manageable and easier to understand, leading to more stable and reliable applications. Technology Diversity: Microservices can be built using different technologies, providing greater flexibility and innovation in the development process. Scalability: Microservices can be scaled independently of each other, making it easier to allocate resources based on demand Flexibility: Microservices can be developed and deployed independently, allowing for more agile development processes and faster time-to-market Resilience: Microservices are independent of each other; failures in one service do not necessarily affect other services. Complexity: Microservices can introduce additional complexity in managing and coordinating interactions between services, and it can be more challenging to test and debug a distributed system. Disadvantages of Microservice Architecture Microservice architecture has its own set of challenges and drawbacks, which include: Complexity: The complexity increases as the number of microservices increases, making it harder to manage and coordinate interactions between services. Skilled developers: Working with microservices architecture requires skilled developers who can identify the microservices and manage their inter-communications effectively. Deployment: The independent deployment of microservices can be complicated, requiring additional coordination and management. Network usage: Microservices can be costly in terms of network usage as they need to interact with each other, resulting in network latency and increased traffic. Security: Microservices may be less secure than monolithic applications due to the inter-services communication over the network, which can be vulnerable to security breaches. Debugging: Debugging can be difficult as control flows over multiple microservices, and identifying where and why an error occurred can be a challenging task. Difference Between Monolithic and Microservice Architecture Monolithic architecture refers to a traditional approach where an entire application is built as a single, unified system, with all the functions tightly coupled together. In this approach, changes to one part of the application can have a ripple effect on other parts, making it difficult to scale and maintain over time. In addition, monolithic architecture is typically deployed as a single, large application, and it can be difficult to add new features or make changes to the system without affecting the entire application. In contrast, microservice architecture is a more modern approach where an application is broken down into smaller, independent services that can be developed, deployed, and scaled independently of each other. Each microservice focuses on a specific function within the larger application, and communication between services is typically accomplished through lightweight communication protocols such as REST or message queues. This approach offers more flexibility and scalability, as changes to one microservice do not necessarily affect the other microservices. Why Golang for Microservices? Go, also known as Golang, is an ideal language for developing microservices due to its unique characteristics. Here are some reasons why you consider Go for building microservices: Performance: Go is a fast and efficient language, which makes it perfect for building microservices that need to handle high volumes of traffic and requests. It also provides excellent runtime performance. Benchmarks game test: fannkuch-redux source secs mem gz cpu secs Go 8.25 10,936 969 32.92 Java 40.61 39,396 1257 40.69 Python 913.87 11,080 385 913.83 Benchmarks game test: n-body source secs mem gz cpu secs Go 6.36 11,244 1200 6.37 Java 7.46 39,992 1430 7.5 Python 383.12 11,096 1196 383.11 Source: Benchmarks Game Concurrency: Go provides built-in support for concurrency, which allows it to handle multiple requests simultaneously, making it a great choice for building scalable microservices. Small footprint: Go binaries have a small footprint and are lightweight, which makes it easier to deploy and manage microservices in a containerized environment. Simple and readable syntax: Go has a simple and intuitive syntax that is easy to learn and read. This makes it a great language for building maintainable microservices that are easy to understand and modify. Strong standard library: Go has a strong standard library that provides many useful built-in functions and packages for building microservices, such as networking, serialization, and logging. Large community: Go has a large and active community that provides support, resources, and third-party libraries for building microservices. Cross-platform support: Go is designed to be cross-platform, which means that you can build and deploy microservices on a variety of operating systems and environments. Overall, Go is an excellent choice for building microservices that require high performance, scalability, and efficiency. Its simple syntax, strong standard library, and large community make it a popular choice among developers for building microservices, and its small footprint and cross-platform support make it easy to deploy and manage microservices in a containerized environment. Golang Frameworks Here are a few frameworks build in Golang: • Go Micro – Go Micro is an open-source platform designed for API-driven development. It is also the most popular remote procedure call (RPC) framework today. It provides load balancing, synchronous and asynchronous communication, message encoding, service discovery, and RPC client/server packages. • Go Kit – Go Kit is a library that fills the gaps left by the standard library and provides solutions for most operational and infrastructure concerns. It makes Go a first-class language for writing microservices in any organization. These frameworks provide a variety of features and functionalities that can help developers build and manage microservices more efficiently. Choosing the right framework will depend on the specific requirements of the project and the expertise of the development team. Why Do Companies Use Golang? Two main reasons why Google's programming language is used by companies of various tiers: Designed for multi-core processing: Golang was specifically designed for cloud computing and takes advantage of modern hardware's parallelism and concurrency capabilities. Golang's goroutines and channel-based approach make it easy to utilize all available CPU cores and handle parallel I/O without adding complexity to development. Built for big projects: Golang's code is simple and easy to read, making it ideal for large-scale projects. Real-Life Examples of Using Go to Build Microservices Here are some real-life examples of companies using Go to build microservices: Netflix: Netflix uses Go for several of its microservices, including its platform engineering and cloud orchestration services. Go's performance and concurrency features help Netflix handle the scale and complexity of its infrastructure. Docker: Docker is a popular containerization platform that uses Go to power many of its core components, including the Docker daemon and Docker CLI. Go's small footprint, and cross-platform support make it well-suited for building containerized applications. Conclusion Microservices have become a popular choice for startups and large companies due to their scalability, sustainability, and ability to deploy them independently. Among programming languages, Go stands out as an excellent choice for microservices. Its easy-to-write code, high level of security, fast execution speed, and low entry threshold make it unmatched for this architecture. Go's performance, concurrency, and simplicity make it an increasingly popular option for building scalable and reliable microservices architectures.
Nuwan Dias
VP and Deputy CTO,
WSO2
Christian Posta
VP, Global Field CTO,
Solo.io
Rajesh Bhojwani
Development Architect,
Sap Labs
Ray Elenteny
Solution Architect,
SOLTECH