Files
School/Anul 3/Semestrul 2/Licenta/Thesis/chapters/chapter5.tex
T
2025-07-03 20:56:38 +03:00

329 lines
24 KiB
TeX

\chapter{Building applications with Microservices Architecture in .NET}
\label{chap:ch4}
\par This chapter delves into the critical aspects of scalability and fault tolerance in the context of microservice-based applications. Using a practical example of an e-commerce platform built with .NET, React, and deployed on Kubernetes, we explore the architectural decisions and implementation details that contribute to a resilient and scalable system. We analyze the role of various technologies and patterns, including API Gateways, asynchronous messaging with RabbitMQ, and the application of resilience policies using Polly. The discussion highlights how these components work in concert to handle fluctuating loads and gracefully manage failures, ensuring a seamless user experience.
\section{Architectural Overview}
The application's architecture is a classic example of a microservice-based system. A user-facing React application and a separate React Admin interface for administrative tasks serve as the clients. All client requests are first routed to an API Gateway built with YARP (Yet Another Reverse Proxy). This gateway acts as a single, unified entry point, simplifying the frontend logic and directing traffic to the appropriate backend microservice.
The core backend logic is distributed across three distinct services:
\begin{itemize}
\item OrderService: Manages the creation and tracking of customer orders.
\item InventoryService: Handles product stock levels and availability.
\item PaymentService: Integrates with Stripe to process payments securely.
\end{itemize}
Communication between these services is handled through two distinct channels. For immediate, request-response interactions, services communicate synchronously via direct HTTP calls. For processes that can be handled in the background, such as updating inventory after a payment, the services communicate asynchronously using RabbitMQ as a message broker. This dual approach optimizes for both responsiveness and resilience.
\begin{figure}[h!]
\centering
\begin{tikzpicture}[
auto,
node distance=2.2cm and 2cm, % Taller distance for vertical layout
font=\small,
% Styles for different components
service/.style={
rectangle, draw, fill=blue!10, rounded corners,
minimum height=1.2cm, minimum width=4cm, text centered, text width=4cm
},
dbs/.style={
cylinder, shape border rotate=90, draw, fill=gray!20,
aspect=0.3, minimum height=1.5cm, text centered, text width=3cm
},
gateway/.style={
rectangle, draw, fill=green!15, rounded corners,
minimum height=1.2cm, minimum width=4.5cm, text centered
},
frontend/.style={
rectangle, draw, fill=orange!15, rounded corners,
minimum height=1.2cm, minimum width=4.5cm, text centered
},
broker/.style={
rectangle, draw, fill=purple!15, rounded corners,
minimum height=4cm, minimum width=2.2cm, text centered
},
external/.style={
rectangle, draw, fill=red!10, rounded corners,
minimum height=1.2cm, minimum width=2.5cm, text centered
},
% Arrow styles
http_arrow/.style={-Stealth, thick},
async_arrow/.style={-Stealth, dashed, thick, blue!60!black}
]
% TIER 1: CLIENT
\node[frontend] (react_app) {React Frontend (User \& Admin)};
% TIER 2: GATEWAY
\node[gateway] (api_gateway) [below=1.5cm of react_app] {API Gateway (YARP)};
% TIER 3: SERVICES (Stacked Vertically)
\node[service] (payment_service) [below=1.5cm of api_gateway] {Payment Service (.NET)};
\node[service] (order_service) [below=of payment_service] {Order Service (.NET)};
\node[service] (inventory_service) [below=of order_service] {Inventory Service (.NET)};
% TIER 4: DATA
\node[dbs] (shared_db) [below=of inventory_service] {Shared DB (SQL Server)};
% SIDE COMPONENTS
\node[external] (stripe) [left=of payment_service] {Stripe API};
\node[broker] (rabbitmq) [right=of order_service] {RabbitMQ};
% Labels for queues inside RabbitMQ node
\node[anchor=center, text width=2cm] (q1) at ([yshift=0.7cm]rabbitmq.center) {\small \textit{purchase\_queue}};
\node[anchor=center, text width=2cm] (q2) at ([yshift=-0.7cm]rabbitmq.center) {\small \textit{inventory\_queue}};
% KUBERNETES CLUSTER BOUNDARY (using fit, updated for vertical layout)
\begin{scope}[on background layer]
\node[
draw=gray, dashed, thick, rounded corners,
fit=(api_gateway) (payment_service) (inventory_service) (shared_db) (rabbitmq),
label={[yshift=0.2cm]above right:Kubernetes Cluster}
] (k8s_cluster) {};
\end{scope}
% --- ARROWS - Communication Flow ---
% Client -> Gateway
\draw[http_arrow] (react_app) -- node[midway, right] {HTTP/S Requests} (api_gateway);
% Gateway -> Services (Proxy)
\draw[http_arrow] (api_gateway) .. controls +(east:4) and +(east:4) .. node[midway, left] {Proxy} (payment_service);
% Use orthogonal paths for other proxy connections to keep it clean
\draw[http_arrow] (api_gateway.south) .. controls +(east:4) and +(east:4) .. node[pos=0.25, left] {Proxy} (order_service.north);
\draw[http_arrow] (api_gateway.south) .. controls +(west:4) and +(west:4) .. node[pos=0.25, left] {Proxy} (inventory_service.north);
% Services -> Database
\draw[http_arrow] (inventory_service) -- node[midway, right] {DB Queries} (shared_db);
% Route the other DB connection around the inventory_service node
\draw[http_arrow] (order_service.south) .. controls +(east:4) and +(east:4) ..node[pos=0.5, right] {DB Queries} (shared_db.north);
% Inter-Service HTTP Communication
\draw[http_arrow] (payment_service) -- node[midway, left] {1. Get Order Details}
node[midway, right, text width=2.5cm] {\footnotesize(Polly: Retry, Circuit Breaker)} (order_service);
% Upward arrow from inventory to order, curved to show reverse flow
\draw[http_arrow] (inventory_service.north) .. controls +(west:2) and +(west:2) ..
node[midway, left] {2. Get Order Details}
node[midway, right] {\footnotesize(Polly)}
(order_service.south);
% Payment Flow
\draw[http_arrow] (payment_service) -- node[midway, above] {Create Session} (stripe);
\draw[http_arrow, gray] (stripe.north) .. controls +(west:1) and +(west:2) ..
node[midway, above, sloped] {Redirect User} (react_app.west);
% Asynchronous Messaging via RabbitMQ
\draw[async_arrow] (payment_service.east) -- node[midway, above] {Publish} (rabbitmq.west);
\draw[async_arrow] (rabbitmq) -- node[midway, above] {Consume} (order_service.east);
\draw[async_arrow] (rabbitmq.south) -- ++(0,-0.5) -| node[pos=0.25, right] {Consume} (inventory_service.east);
\end{tikzpicture}
\caption{The microservice architecture for the e-commerce application. It demonstrates a vertical flow from the API Gateway to the services, which use a shared database. Asynchronous communication via RabbitMQ and resilient inter-service calls with Polly are key features for fault tolerance.}
\label{fig:architecture}
\end{figure}
\section{Implementing Scalability}
Scalability is addressed at multiple levels within this architecture, from the infrastructure to the application logic itself.
\subsection{Service-Level Scalability with Kubernetes}
The entire backend is designed for a cloud-native environment, with each microservice containerized and deployed on Kubernetes. This platform provides a powerful framework for achieving automated, horizontal scalability.
The deployment of each service is managed by three key Kubernetes objects. First, a Deployment manifest defines the desired state of the service. It specifies the container image to use, resource requests and limits to ensure predictable performance, and any necessary environment configurations.
Second, a Service manifest exposes the pods managed by the Deployment to the network. It provides a stable IP address and DNS name, and it automatically load-balances incoming traffic across all available replicas of the microservice. In this application, the InventoryService/OrderService/PaymentService is exposed via a LoadBalancer service, making it accessible from outside the cluster on a specific port.
Finally, a HorizontalPodAutoscaler (HPA) automates the scaling process. The InventoryService/OrderService/PaymentService is configured with an HPA that monitors its CPU utilization. The system is set to maintain a minimum of one running instance but can automatically scale up to a maximum of three instances.\cite{kubernetes2025horizontal} \cite{baboi2019dynamic} A scale-out event is triggered whenever the average CPU utilization across all pods exceeds 80\%, ensuring that the application can seamlessly handle sudden spikes in traffic without manual intervention. (See Figure \ref{fig:hpa_mechanism_horizontal} for a visual representation of the HPA mechanism.)
\begin{figure}[h!]
\centering
\begin{tikzpicture}[
auto,
node distance=1.5cm and 2.2cm,
font=\small,
% Styles
pod/.style={
rectangle, draw, fill=blue!10, minimum height=1cm, minimum width=3cm, text centered
},
k8s_service/.style={
rectangle, draw, fill=green!15, minimum height=3cm, minimum width=2.5cm, text centered, text width=2.5cm
},
hpa/.style={
rectangle, draw, rounded corners, fill=purple!15, minimum height=1cm, minimum width=3cm, text centered
},
metrics_server/.style={
rectangle, draw, fill=yellow!20, minimum height=1cm, minimum width=3cm, text centered
},
deployment/.style={
rectangle, draw=gray, dashed, inner sep=0.6cm, rounded corners
},
% Arrow styles
flow_arrow/.style={-Stealth, thick},
control_arrow/.style={-Stealth, thick, red!80!black},
metric_arrow/.style={-Stealth, dashed, gray}
]
% --- Main traffic flow (Horizontal) ---
% K8s Service (Load Balancer) on the left
\node[k8s_service] (service_lb) {Kubernetes Service (Load Balancer)};
% Vertically stacked pods to the right
\node[pod] (pod1) [right=of service_lb] {Pod 1};
\node[pod] (pod2) [below=0.5cm of pod1] {Pod 2};
\node[pod] (podN) [below=0.5cm of pod2] {Pod N};
% Deployment around Pods
\node[deployment, fit=(pod1) (pod2) (podN), label={[xshift=0.3cm]right:Deployment}] (dep) {};
% --- Autoscaling Loop (Below) ---
\node[metrics_server] (metrics) [below=of dep, yshift=-1cm] {Metrics Server};
\node[hpa] (hpa_controller) [left=of metrics] {HPA};
% --- ARROWS ---
% Incoming traffic to Service
\draw[flow_arrow] ++(-2.5, 0) node[left, text width=1.5cm]{Incoming Traffic} -- (service_lb);
% Service to Deployment/Pods
\draw[flow_arrow] (service_lb) -- (dep);
% --- Arrows for Scaling Loop ---
% Pods (Deployment) to Metrics Server
\draw[metric_arrow] (dep.south) -- node[midway, right] {CPU/Memory Metrics} (metrics);
% Metrics to HPA
\draw[metric_arrow] (metrics) -- node[midway, above] {Metrics} (hpa_controller);
% HPA to Deployment (curved arrow to close the loop)
\draw[control_arrow] (hpa_controller.north) .. controls +(north:1.5) and +(west:1.5) ..
node[pos=0.3, below, sloped] {Scale Up/Down} (dep.west);
\end{tikzpicture}
\caption{A horizontally oriented view of the Kubernetes autoscaling mechanism. Incoming traffic is directed by the Kubernetes Service to the Deployment, which manages a scalable set of Pods. The Horizontal Pod Autoscaler (HPA) forms a control loop below: it receives metrics from the Pods via the Metrics Server and adjusts the number of replicas in the Deployment accordingly.}
\label{fig:hpa_mechanism_horizontal}
\end{figure}
\subsection{Asynchronous Communication with RabbitMQ}
The use of RabbitMQ for inter-service communication is a cornerstone of the application's scalability strategy. \cite{rabbitmq2025messaging} When an order is placed and payment is completed, the system does not rely on a chain of direct, blocking calls. Instead, the PaymentService publishes messages to designated queues in RabbitMQ.
This decouples the services, allowing them to operate and scale independently. The OrderService and InventoryService each contain a background consumer that listens for relevant messages. This asynchronous, event-driven pattern prevents the PaymentService from being blocked while waiting for downstream services to confirm updates. The result is improved responsiveness for the end-user and higher overall system throughput, as each service can process tasks at its own pace.
\section{Resilient Communication with Polly}
The .NET resilience and transient-fault-handling library, Polly, is integrated into the application's HTTP clients to gracefully handle the inevitable failures in a distributed system.
\subsection{The Retry Pattern}
To combat transient network issues or temporary service unavailability, a Retry policy is implemented. This policy automatically re-attempts a failed HTTP request a specified number of times. To avoid overwhelming a struggling service, it uses an exponential backoff strategy, where the delay between each retry attempt increases progressively. This simple yet effective pattern significantly improves the reliability of inter-service communication. \cite{polly2025dotnet}
\subsection{The Circuit Breaker Pattern}
To prevent cascading failures, where a problem in one service brings down others, the Circuit Breaker pattern is employed. This policy monitors calls to a downstream service. If the number of consecutive failures surpasses a configured threshold (configured as 10 failed requests), the circuit "opens," and all subsequent calls fail immediately, without making a network request. This protects the calling service from wasting resources on calls that are likely to fail. After a timeout period, the circuit enters a "half-open" state, allowing a single test request through. If this request succeeds, the circuit "closes," and normal operation resumes. Otherwise, it remains open, giving the failing service more time to recover. \cite{polly2025dotnet}
\subsection{Fault Tolerance with Kubernetes}
Kubernetes itself provides a powerful, declarative foundation for fault tolerance. Its control plane continuously works to ensure the actual state of the cluster matches the desired state defined in the configuration files.
\begin{itemize}
\item \textbf{Self-Healing:} If a container or pod crashes for any reason, Kubernetes automatically detects the failure and restarts it or provisions a new one, ensuring the service remains available without manual intervention.
\item \textbf{Rolling Updates:} When deploying a new version of a service, Kubernetes performs a rolling update. It gradually replaces old pods with new ones, ensuring there is no downtime. It also monitors the health of the new pods and can automatically roll back to the previous version if they fail to start correctly.
\end{itemize}
\subsection{Message Durability with RabbitMQ}
For true fault tolerance in a production system, the RabbitMQ queues are configured as durable. This ensures that messages persist on disk and will not be lost even if the RabbitMQ server restarts. The application already implements manual message acknowledgments, meaning a message is only removed from the queue after the consumer service has successfully processed it. This combination of durable queues and acknowledgments guarantees that no critical business event, like an inventory update, is lost due to a transient failure.
\section{Observability: Metrics and Monitoring}
While scalability and fault tolerance mechanisms provide the foundation for a robust system, they are incomplete without a comprehensive observability strategy. In a distributed microservices architecture, understanding the internal state and performance of the system is paramount for maintaining reliability.
The application employs a modern observability stack consisting of Prometheus for metrics collection and Grafana for visualization. \cite{prometheus2025monitoring} \cite{grafana2025open}
Each .NET microservice is instrumented using the prometheus-net library, which exposes a standard /metrics endpoint. This endpoint provides a wealth of real-time data, including HTTP request rates, error percentages, and response latencies, as well as .NET runtime statistics like garbage collection and CPU usage.
To collect this data, Prometheus is deployed within the Kubernetes cluster. The collection process is automated using ServiceMonitor resources. For each microservice (InventoryService, OrderService, and PaymentService), a ServiceMonitor tells the Prometheus instance how to discover and "scrape" (or pull) the data from its /metrics endpoint. This declarative approach integrates seamlessly with Kubernetes service discovery, ensuring that as services scale up or down, all new instances are automatically included in the monitoring. \cite{prometheus2025monitoring}
While Prometheus is the engine for collecting and storing this time-series data, Grafana serves as the visualization layer. Operators and developers can build detailed dashboards in Grafana to plot key metrics over time. \cite{grafana2025open} These dashboards provide at-a-glance visibility into the health of the entire system. For example, one could visualize the CPU utilization of the InventoryService pods to see the Horizontal Pod Autoscaler in action, or monitor the HTTP 5xx error rate across all services to detect failures. This ability to monitor, visualize, and alert on key performance indicators is not just a reactive tool for debugging but a proactive one for identifying performance bottlenecks and potential failures before they impact users.
\section{Application User Interface and Workflow}
This section details the user-facing and administrative interfaces of the e-commerce application, illustrating the typical user journey from browsing products to completing a purchase, as well as the administrative workflow for managing inventory.
\subsection{Customer-Facing Storefront}
The customer experience is designed to be clean, intuitive, and efficient, guiding the user from product discovery to checkout with ease.
\begin{itemize}
\item Product Catalog View (Homepage): The main entry point for customers is a modern storefront that displays products in a clear grid layout. Each item is presented as a distinct card containing a product image, name, a brief description, and the price. An "Add to Cart" button is prominently featured on each card, inviting interaction. On the right-hand side of the page, a persistent "Shopping Cart" module provides an at-a-glance summary of the items the user has selected. On the initial visit, this cart correctly indicates that it is empty. Figure \ref{fig:product_catalog}
\begin{figure}
\centering
\includegraphics[width=0.8\textwidth]{figures/products.png}
\caption{Product catalog view, showcasing products in a grid layout with an "Add to Cart" button and a persistent Shopping Cart module on the right.}
\label{fig:product_catalog}
\end{figure}
\item Adding Items to the Cart: When a user clicks the "Add to Cart" button for an item, the interface provides immediate visual feedback. The selected product is instantly added to the Shopping Cart module on the right. The cart dynamically updates to show each unique item, its quantity (which can be incremented), and its price. A subtotal, calculated tax, and a final, bolded total are displayed in real-time. This seamless and interactive experience allows users to track their selections and total cost without navigating to a separate page. Figure \ref{fig:shopping_cart}
\begin{figure}
\centering
\includegraphics[width=0.5\textwidth]{figures/checkout.png}
\caption{Shopping Cart view, showing selected products with quantities, prices, and a total cost. The "Proceed to Checkout" button is highlighted at the bottom.}
\label{fig:shopping_cart}
\end{figure}
\item Checkout Process: Once the user has finished shopping, they click the "Proceed to Checkout" button located at the bottom of the shopping cart. This action signifies the handoff from browsing to payment and redirects the user away from the application's storefront to a secure, externally hosted payment page powered by Stripe. Figure \ref{fig:stripe_payment}
\begin{figure}
\centering
\fbox{\includegraphics[width=0.5\textwidth ]{figures/stripe.png}}
\caption{Stripe Payment Page, displaying a final order summary on the left and a secure form for entering payment details on the right. \cite{stripe2025online}}
\label{fig:stripe_payment}
\end{figure}
\item Stripe Payment Page: The application's integration with Stripe Checkout is seamless. On the secure Stripe page, the user sees a final order summary on the left, itemizing the products and the total charge. On the right, they are presented with a standard, secure form to enter their email and credit card information. This critical design choice offloads the responsibility of handling and securing sensitive payment card industry (PCI) data to a trusted, compliant third-party provider, which is a best practice that significantly enhances the security and trustworthiness of the application. \cite{stripe2025online} Figure \ref{fig:stripe_payment}
\end{itemize}
\subsection{Administrative Interface}
A separate, secure web application serves as the administrative backend, allowing store managers to perform essential inventory management tasks.
\begin{itemize}
\item Inventory Dashboard: The primary administrative screen is the "Inventory Dashboard." It features a clean, tabular view of the entire "Product Catalog." Each row in the table corresponds to a single product, clearly displaying its photo, name, description, price, and the current quantity in stock. To facilitate easy management, each row includes "Actions" buttons—an icon for editing and an icon for deleting the product. At the top of the catalog, an "Add Product" button allows for the creation of new inventory items. Figure \ref{fig:inventory_dashboard}
\begin{figure}
\centering
\fbox{\includegraphics[width=0.5\textwidth ]{figures/inventory.png}}
\caption{Inventory Dashboard, displaying a table of products with options to edit, delete, or add new products. Each product row includes a photo, name, description, price, and quantity.}
\label{fig:inventory_dashboard}
\end{figure}
\item Editing a Product: When an administrator clicks the edit icon for a product, a modal form titled "Edit: [Product Name]" appears, overlaying the dashboard. This form is pre-populated with all the existing product data (name, description, photo URL, price, and quantity). The administrator can modify any of these fields and then click the "Update Product" button to save the changes to the database or "Cancel" to discard them. This modal approach provides a quick and efficient workflow for making targeted updates to product details and stock levels without losing the context of the main dashboard. Figure \ref{fig:edit_product}
\begin{figure}
\centering
\fbox{\includegraphics[width=0.5\textwidth ]{figures/edit.png}}
\caption{Edit Product Modal, allowing administrators to modify product details. The form is pre-populated with existing data, and changes can be saved or canceled.}
\label{fig:edit_product}
\end{figure}
\item Adding a New Product: Clicking the "Add Product" button opens a similar modal form, but this one is empty, allowing the administrator to enter all necessary details for a new product. After filling out the form, they can click "Create Product" to add it to the catalog or "Cancel" to close the modal without saving. Figure \ref{fig:edit_product}
\item Deleting a Product: The delete action is straightforward. When the administrator clicks the delete icon for a product, a confirmation dialog appears, asking if they are sure they want to delete the product. This step prevents accidental deletions and ensures that the administrator has a chance to confirm their intent.
\end{itemize}
This clear separation of concerns between a public-facing storefront and a secure administrative backend is a standard and effective design pattern. It ensures a simple, intuitive shopping experience for customers while providing powerful and secure tools for store managers to maintain the product catalog.