Back to: Microservices using ASP.NET Core Web API Tutorials
Microservices Design Principles
In this article, we will explore the core Design Principles of Microservices, including the Single Responsibility Principle, Independent Deployment, Decentralized Data Management, Loose Coupling, High Cohesion, Event-Driven Communication, Fault Isolation, Scalability, and Technology Diversity, with practical examples based on a real-world e-commerce application.
Understanding and applying these principles ensures that microservices remain maintainable, resilient, and ready to support future growth. The focus is on ensuring each service has a clear responsibility, can be deployed independently, manages its own data, and interacts with others through loosely coupled, event-driven communication.
Overview of the E-Commerce Application and Microservices
Imagine a modern e-commerce platform built to handle thousands of daily users, a rich product catalog, secure payments, and real-time order processing. To achieve flexibility, scalability, and rapid evolution, the platform is architected as a set of specialized microservices. Each service is responsible for a single business function, communicates over well-defined interfaces, and manages its own data. For a better understanding, please have a look at the following diagram:
User Service
Manages user registration, authentication, profile management, and security.
- Functionality: Handles user registration, login, profile updates, password resets, and security enhancements like two-factor authentication.
- Data Ownership: Storing user data in its own database, such as SQL Server.
- Interaction: Exposes APIs that other services (such as Order or Payment) consume to retrieve user-related information.
Product Service
Manages the product catalog, including product details, inventory, and categorization.
- Functionality: Allows creation, updates, and removal of products, manages stock levels, and supports search and filtering capabilities.
- Data Ownership: Owns a dedicated database optimized for product data, such as NoSQL for flexibility and performance.
- Interaction: Provides APIs for querying product information used by Order Service or user-facing client applications.
Order Service
Manages the full order lifecycle, from creation to shipment.
- Functionality: Handles order placement, status tracking (Pending, Packed, Shipped, Delivered), and order validation.
- Data Ownership: Maintains its own relational database to store order records, statuses, and related metadata.
- Interaction: Calls User Service to fetch customer details, Product Service to verify stock and pricing, and Payment Service to initiate payment processing.
Payment Service
Manages payment processing, transaction validation, refunds, and payment status tracking.
- Functionality: Supports multiple payment gateways, manages secure transactions, and processes refunds.
- Data Ownership: Keeps a secure and transactional database to record payment history and statuses.
- Interaction: Exposes APIs for payment initiation from Order Service and emits payment success or failure events that other services consume.
Notification Service
Manages all system notifications and messaging (emails, SMS, push notifications).
- Functionality: Listens for domain events such as UserRegistered, OrderPlaced, or PaymentSuccessful and sends appropriate messages asynchronously.
- Data Ownership: Manages templates and notification logs within its own database.
- Interaction: Decoupled from the core flow; reacts to events generated by other services.
Now, let’s try to understand the Core Microservices Design Principles by examining the above e-commerce example with multiple Microservices.
Single Responsibility Principle (SRP)
SRP means that each microservice should have only one clear, well-defined job or responsibility.
- It should own all the logic, data, and behaviour for that job.
- It should not mix unrelated tasks or multiple business functions into a single service.
For a better understanding, please have a look at the following image:
Examples:
- User Service focuses only on users. It does NOT handle orders or payments.
- Product Service focuses only on products. It does NOT process orders or manage payments.
- Order Service handles the entire order lifecycle, but it does NOT handle user authentication or payment processing.
- Payment Service deals strictly with payments. It does NOT manage order details or user profiles.
What if SRP is Violated?
Imagine if the Order Service also handled user registration and payment processing:
- The service would become complex, difficult to maintain, and changes in one area (such as user login) might disrupt order processing.
- Scaling the service would be inefficient because different business functions have different load and resource needs.
- Deployment cycles would be slower as unrelated changes require redeploying the whole service.
Independent Deployment
Independent Deployment means each microservice can be:
- Developed,
- Tested,
- Deployed, and
- Updated
separately from the others. This means you can make changes or fix bugs in one service without needing to redeploy or change any other service. The following diagram illustrates this concept clearly.
Product Service
Suppose you add a new feature, Bulk Product Import, to the Product Service (allowing admins to upload a CSV of new products). Here, Independent Deployment means:
- You can build, test, and deploy this new feature to the Product Service only.
- No need to touch or redeploy the Order, User, or Payment Services.
Benefit: If something goes wrong, only the Product Service is affected, not the whole system.
Order Service
Suppose you fix a bug in how orders are tracked (e.g., correcting order status updates). Here, Independent Deployment means:
- Only the Order Service is updated and redeployed.
- The Payment, User, and Product Services continue to run as they were.
Benefit: Fast bug fix, no system downtime, and no need for cross-team coordination.
Payment Service
Suppose you add a new payment method (e.g., enable UPI payments). Here, Independent Deployment means:
- The Payment Service team develops, tests, and deploys the update.
- As long as the API contract used by other services doesn’t change, nothing else is affected.
Benefit: Users can access the new payment option immediately, without delay, and without waiting for other teams.
User Service
Suppose you improve user security by adding two-factor authentication. Here, Independent Deployment means:
- You can deploy the new login flow to the User Service only.
- The Order, Payment, and Product Services remain untouched and unaffected.
Benefit: Security enhancements are rolled out rapidly without halting or coordinating with unrelated teams.
Decentralized Data Management
In a microservices architecture, decentralized data management means:
- Each microservice owns and manages its own private data store (could be a database, cache, or file storage).
- No service directly reads or writes data from another service’s database.
- When a service needs data owned by another, it accesses it only via well-defined APIs or message brokers.
This ensures clear ownership of data, reduces dependencies, and prevents tight coupling through shared databases. Let’s visualize this with a simple diagram.
Order Service
The Order Service maintains its own Order Database (e.g., SQL), storing order details such as order ID, product IDs, user ID, order status, and timestamps. It stores everything about the order itself, including when it was placed, the products included in the order, and its current status (Pending, Shipped, Delivered).
- Accessing user information: When the Order Service needs a customer’s name or address, it calls the User Service API to retrieve that information; it does not query the User Service’s database directly.
- Accessing product information: To confirm product price or availability, the Order Service calls the Product Service API, not the Product database.
User Service
User Service has its own User Database (e.g., NoSQL or relational) that stores user credentials, profiles, addresses, and preferences. It manages user sign-ups, authentication, and profile updates.
- It exposes APIs that allow other services (such as Order or Payment) to request user details securely.
- No other service can read or write data directly in this database; they must go through the User Service’s APIs.
Product Service
The Product Service has its own Product Database (e.g., a NoSQL database optimized for catalog data) containing product descriptions, stock levels, categories, and prices. It handles the addition and removal of products, as well as updating inventory counts.
- It exposes Product information on request through APIs.
- Other services (such as Order Service) never access this database directly; instead, they rely on the API.
Payment Service
The Payment Service maintains its own payment database, which stores payment transactions, statuses, and receipts. It processes payments, manages refunds, and records transaction history.
- When a payment succeeds, it creates an event or sends a message saying PaymentCompleted for the Order Service to consume.
- It does not directly update the Order database or user info; it only shares payment results via APIs/events.
Loose Coupling
Loose coupling means that each microservice can work independently, with minimal knowledge of the internal workings of other services. They:
- Communicate only through well-defined interfaces like APIs or Message Brokers (not by sharing databases or code).
- Ensure that changes in one service don’t force changes in others.
- Can be developed, deployed, or scaled independently without breaking the whole system.
For a better understanding, please refer to the following image.
Example 1: Order Service
The Order Service handles placing, updating, and tracking orders. Let us understand how it interacts with other services:
- When a new order is placed, it calls the Product Service API to check if items are in stock.
- It then calls the Payment Service API to initiate payment.
- It does not access the other services databases or internal logic directly.
Loose coupling in action: If the Payment Service changes how it processes transactions (say, adds new payment gateways), as long as its API contract remains the same, the Order Service doesn’t need to change.
Example 2: Payment Service
Processes payments and refunds. Let us understand how it interacts with others:
- It exposes an API: POST /payments with required order and user details.
- It doesn’t need to know how orders are created or how products are managed; it just needs the payment request via the API.
Loose coupling in action: If the Payment Service updates its internal logic (say, improves fraud checks), the Order Service and User Service remain unaffected.
Example 3: Product Service
Manages product information (add, remove, update products). Let us understand how it interacts with others:
- Provides an API to retrieve product details and stock information.
- Other services (like Order Service) request data using this API.
- If the Product Service changes its database or tech stack, as long as the API contract remains the same, other services will continue to work without change.
Loose coupling in action: The Product Service could switch from MySQL to MongoDB, but other services don’t care; they use the API.
Example 4: User Service
Handles user registration, authentication, and profiles. Let us understand how it interacts with others:
- Exposes APIs for creating accounts and fetching user details.
- Other services (Order, Payment) reference users by their ID or token, but don’t manage user data directly.
Loose coupling in action: If the User Service adds two-factor authentication, other services can still fetch user details via the same API.
High Cohesion
High cohesion means that the functions (actions) and data inside a microservice are closely related and focused on a single responsibility or domain area. In other words:
- Everything inside the service belongs together logically.
- The service does one well-defined job, and all its parts support that job.
- It avoids mixing unrelated tasks or data inside the same service.
The following diagram illustrates this concept clearly.
Product Service:
Manage everything about products.
- When an admin wants to add a new phone or shirt to the online store, this microservice handles creating the product entry with its details (name, price, description, category, etc.).
- If a product is discontinued or out of stock forever, this service deletes or deactivates it.
- It doesn’t handle user registration, payment processing, or order tracking.
Order Service:
Handle everything related to customer orders.
- When a customer clicks Buy Now, this service creates an order record, reserves the items, and marks them as ordered.
- As the order moves through steps (placed → packed → shipped → delivered), this service updates the status.
- It doesn’t process payments, manage product data, or user profiles.
Payment Service:
Managing everything about payments and transactions.
- When a customer pays for their order, the Payment Service handles the payment gateway, ensures the transaction is successful, and logs the transaction details.
- It is dedicated only to payment operations.
- It doesn’t track purchased products, manage orders, or create user accounts.
User Service:
Manage users of the application.
- When a new user signs up, the User Service handles account creation, and later if the user updates their address or password, this service manages those profile changes.
- It deals only with user data and authentication.
- It doesn’t process orders, handle payments, or manage products.
Event-Driven Communication
In an event-driven architecture, microservices communicate using asynchronous events or message passing rather than making synchronous direct calls. This enables each service to respond to system changes independently and asynchronously, thereby improving scalability. Let’s visualize this with a simple diagram.
Example 1: Order Service Creates an OrderPlaced Event
- Order Service: When a customer places a new order, the Order Service saves the order and creates an OrderPlaced event containing order details.
- Product Service: Subscribes to the OrderPlaced event. When received, it reduces the product stock levels accordingly (reserves items).
- Payment Service: Listens for the same OrderPlaced event. When received, it initiates the payment process for that order.
The Product and Payment services process the event independently and asynchronously, so the Order Service doesn’t have to wait for payment or stock updates to finish, improving responsiveness and scalability.
Example 2: Payment Service Creates PaymentSuccessful Event
- Payment Service: After successfully processing payment, it emits a PaymentSuccessful event.
- Order Service: Subscribes to the PaymentSuccessful event. When received, it updates the order status to ‘Paid’ and triggers further processing, such as shipment.
- User Service: Listens to the PaymentSuccessful event and then awards loyalty points or sends payment confirmation to the user.
The Payment processing is decoupled from order management and user notifications. Each reaction happens independently.
Example 3: User Service and Notification Service
- User Service: When a user completes registration, the User Service publishes a UserRegistered event.
- Notification Service: A separate Notification Service listens to this event and sends a welcome email or SMS to the new user.
This way, the User Service is not delayed by email processing, ensuring a smooth user signup experience.
Fault Isolation
In microservices architecture, fault isolation means that if one microservice fails or experiences problems, the failure is contained within that service and does not cause other services or the entire system to crash. This improves the overall system’s resilience and availability. For a better understanding, please refer to the following image.
Example 1: Payment Service Failure Does Not Stop Orders
The Payment Service becomes unavailable (due to a bug, third-party gateway outage, etc.). In this case:
- The Order Service continues to accept and record new orders.
- When the Order Service attempts to contact the Payment Service, a circuit breaker or retry pattern detects repeated failures and temporarily halts further payment requests.
- The system notifies users that payment is pending and will be processed once the Payment Service is back online.
Result: Users can still browse products, register, and place orders. Only payment processing is delayed, and the rest of the system functions normally.
Example 2: Product Service Failure
The Product Service database goes down or becomes very slow. In this case:
- The Order Service uses a timeout and retry pattern when requesting product info.
- If the Product Service remains unavailable, the Order Service shows an error for product lookup but still allows users to access their past orders or account info.
- The User and Payment Services continue to work as normal (e.g., login, viewing profile, and checking payment status on past orders).
Result: Only product-related features are affected; users can still register, log in, and view orders.
Example 3: Order Service Problem is Isolated
- The Order Service encounters an issue and is unable to create new orders. In this case:
- Product Service continues to update inventory or product data.
- Payment Service continues to process pending payments for previous orders.
- User Service remains fully operational.
Result: Only order creation is paused; other activities are not blocked.
Example 4: User Service Unavailable
Suppose the User Service is down during a user login or profile update request. In this case:
- Other services, like Order or Payment, that rely on User Service API calls implement retries with exponential backoff or fallback responses.
- Product browsing, adding items to the cart, and checking payment status for existing orders remain functional for logged-in users.
- New users cannot register or log in until User Service is restored, but existing sessions and services continue unaffected.
Result: This avoids system-wide failure while the User Service recovers.
Scalability
In a microservices architecture, scalability means that each microservice can be scaled independently (either scaled up or scaled out) based on its own resource needs and workload, rather than scaling the entire application as a single large unit. This enables the efficient use of resources and improved performance under varying loads. This also makes your application more cost-effective and responsive. The following diagram illustrates this concept clearly.
Example 1: Order Service Scaling During a Big Sale
During high sales periods (festive sales or Black Friday), the Order Service experiences heavy traffic due to many customers placing orders simultaneously. In this case:
- Only the Order Service needs to handle this sudden spike in traffic.
- You can launch multiple instances (servers or containers) of the Order Service to meet demand.
- Payment, Product, and User Services run as usual, scaled only as much as needed.
Result: You avoid wasting resources by scaling only what’s necessary, keeping costs and complexity under control.
Example 2: Payment Service Scaling for Flash Sales
A limited-time flash sale causes a surge in online payments. In this case:
- The payment service becomes a bottleneck due to the high transaction volume.
- You add more resources or instances just for the Payment Service (e.g., more payment workers).
- Other services, such as User or Product, may not need extra capacity.
Result: Payments are processed quickly, users don’t experience delays, and you don’t over-provision unrelated services.
Example 3: Product Service Scaling During Catalog Updates
When onboarding many new products (for example, a new vendor uploads thousands of items). In this case:
- You temporarily scale up the Product Service to handle bulk imports and indexing.
- Order, Payment, and User Services remain at normal capacity.
Result: The system remains responsive, with no impact on order placement or payments during product updates.
Example 4: User Service Scaling for Viral Marketing
After a successful ad campaign, thousands of new users register simultaneously. In this case:
- You scale up the User Service to handle the registration and login surge.
- Order, Product, and Payment Services don’t need to be scaled unless their traffic increases too.
Result: New users can register quickly without affecting ordering, payments, or product browsing.
Technology Diversity
Microservices architecture enables teams to select the most suitable technology stack, programming language, database, and tools for each microservice individually. This flexibility allows optimization of each service based on its unique functional and non-functional requirements. This flexibility helps each service achieve optimal performance, scalability, and developer productivity. Let’s visualize this with a simple diagram.
Example 1: Payment Service
Payment processing requires strong reliability, security, and data consistency, which SQL Server and ASP.NET Core handle well.
- Language & Framework: C# with ASP.NET Core Web API
- Database: SQL Server for transactional integrity and ACID compliance
Example 2: Order Service
Order processing requires complex workflows and status tracking with relational data storage, benefiting from Oracle’s advanced capabilities and Java’s robust ecosystem.
- Language & Framework: Java with Spring Boot
- Database: Oracle Database for reliable transaction management and strong data consistency
Example 3: User Service
User Service requires secure and scalable user management, along with fast session handling.
- Language & Framework: C# with ASP.NET Core Identity
- Database: SQL Server for user profiles and authentication data
Example 4: Product Service
A product catalog requires a flexible schema and powerful search capabilities that relational databases may not efficiently support.
- Language & Framework: C# with ASP.NET Core Web API
- Database: Elasticsearch or Azure Cosmos DB (NoSQL) for flexible, scalable product catalog and fast full-text search.
Designing microservices around clear principles such as the Single Responsibility Principle, Independent Deployment, Decentralized Data Management, Loose Coupling, and Event-Driven Communication creates systems that are resilient, scalable, and maintainable. The E-Commerce Application example clearly demonstrates how separating concerns into dedicated microservices, such as User, Product, Order, Payment, and Notification, allows independent development and evolution.
In the next article, I will discuss Clean Architecture in ASP.NET Core Web API with Examples. In this article, I explain Core Microservices Design Principles. I hope you enjoy this article, Core Microservices Design Principles.