Friday, August 21, 2026

Service Life Cycle in .NET Core Application

 


Transient vs Scoped vs Singleton with Real-Time E-Commerce Example

Dependency Injection (DI) is one of the most important concepts in ASP.NET Core. It helps us create loosely coupled, maintainable, testable, and scalable applications.

One of the most frequently asked .NET interview questions is:

What is Service Lifetime in .NET Core? Explain Transient, Scoped, and Singleton with a real-time example.

To understand this properly, we need to understand what happens to a service from the moment it is requested until the moment it is destroyed.


1. What is Service Life Cycle?

A Service Life Cycle defines:

  • When an object is created

  • How long that object lives

  • Whether the same object is reused

  • When the object is destroyed

In ASP.NET Core, the built-in Dependency Injection container provides three primary service lifetimes:

  1. Transient

  2. Scoped

  3. Singleton

The basic idea is:

Application Starts
       |
       v
DI Container Created
       |
       v
Request Comes
       |
       v
Controller / Service Requests Dependency
       |
       v
DI Container Checks Service Lifetime
       |
       +-------------------+
       |                   |
   Transient           Scoped
       |                   |
New instance          One per Request
       |                   |
       +---------+---------+
                 |
              Singleton
                 |
          One per Application

2. Dependency Injection in ASP.NET Core

Suppose we have an e-commerce application.

A request comes to:

GET /api/orders/1001

The request reaches the controller:

public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
}

ASP.NET Core needs to create IOrderService.

It looks into the DI container and asks:

How is IOrderService registered?
What is its lifetime?
Should I create a new instance?
Can I reuse an existing instance?

For example:

builder.Services.AddScoped<IOrderService, OrderService>();

This tells ASP.NET Core:

Create one OrderService instance for each HTTP request and reuse it throughout that request.


3. The Three Service Lifetimes

The three important registrations are:

services.AddTransient<IService, Service>();

services.AddScoped<IService, Service>();

services.AddSingleton<IService, Service>();

Their behavior is different.

LifetimeInstance CreationLifetime
TransientNew instance every time requestedVery short
ScopedOne instance per HTTP requestRequest lifetime
SingletonOne instance for applicationApplication lifetime

Let's understand each one with a real-time example.


4. Transient Lifetime

Transient means:

A new object is created every time the service is requested from the DI container.

Registration:

builder.Services.AddTransient<IEmailService, EmailService>();

Suppose:

public interface IEmailService
{
    void SendEmail();
}

Implementation:

public class EmailService : IEmailService
{
    public EmailService()
    {
        Console.WriteLine("EmailService Created");
    }

    public void SendEmail()
    {
        Console.WriteLine("Email Sent");
    }
}

Now imagine:

public class OrderService
{
    private readonly IEmailService _emailService;

    public OrderService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}

Another service also requests:

public class NotificationService
{
    private readonly IEmailService _emailService;

    public NotificationService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}

Because EmailService is transient:

OrderService
     |
     +---- EmailService Instance #1

NotificationService
     |
     +---- EmailService Instance #2

Two different objects are created.


5. Real-Time Use Cases for Transient

Transient is suitable for lightweight, stateless services.

Examples:

Email Formatter
Message Formatter
DTO Mapper
Validation Service
Small Calculation Service
Data Transformation Service

For example:

builder.Services.AddTransient<IPriceCalculator, PriceCalculator>();

Every time IPriceCalculator is requested, a new instance can be created.


6. Scoped Lifetime

Scoped is probably the most commonly used lifetime in ASP.NET Core Web API applications.

Registration:

builder.Services.AddScoped<IOrderService, OrderService>();

Scoped means:

One instance is created for a particular scope. In ASP.NET Core Web API, the scope normally corresponds to one HTTP request.

Consider:

POST /api/orders

The request enters ASP.NET Core.

ASP.NET Core creates a request scope.

HTTP Request
     |
     v
Request Scope Created
     |
     +---- OrderService
     |
     +---- CustomerService
     |
     +---- Repository
     |
     +---- DbContext
     |
     v
Request Completed
     |
     v
Request Scope Destroyed

All scoped services requested during that request can share their scoped instance.


7. Real-Time Example – E-Commerce Order

Consider an e-commerce application.

The request:

POST /api/orders

requires:

OrdersController
       |
       v
OrderService
       |
       v
OrderRepository
       |
       v
ApplicationDbContext

Registration:

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<ApplicationDbContext>();

During Request #1:

Request #1

OrderService       ---> Instance A
OrderRepository    ---> Instance A
DbContext          ---> Instance A

Another request arrives:

POST /api/orders

Request #2 gets different scoped objects:

Request #2

OrderService       ---> Instance B
OrderRepository    ---> Instance B
DbContext           ---> Instance B

Therefore:

Request #1
   |
   +-- OrderService A
   +-- Repository A
   +-- DbContext A


Request #2
   |
   +-- OrderService B
   +-- Repository B
   +-- DbContext B

This isolation is one of the major reasons why DbContext is normally registered as Scoped.


8. Why DbContext is Usually Scoped

Consider an order transaction:

Create Order
     |
     +-- Insert Order
     |
     +-- Insert Order Items
     |
     +-- Update Inventory
     |
     +-- SaveChanges()

The same DbContext can track entities involved in that request.

For example:

public class OrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task CreateOrder(Order order)
    {
        _context.Orders.Add(order);

        await _context.SaveChangesAsync();
    }
}

Using a scoped context helps keep the database unit of work associated with the request.


9. Singleton Lifetime

Singleton means:

Only one instance of the service is created for the application's DI container lifetime, and that instance is reused wherever the service is requested.

Registration:

builder.Services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();

Conceptually:

Application Starts
       |
       v
Singleton Instance Created
       |
       +-----------------------+
       |                       |
Request 1                  Request 2
       |                       |
       +----------+------------+
                  |
          Same Singleton

The same object can be reused across requests.


10. Real-Time Singleton Example

Suppose our application has application-wide configuration.

public interface IApplicationConfiguration
{
    string ApplicationName { get; }
}

Implementation:

public class ApplicationConfiguration : IApplicationConfiguration
{
    public string ApplicationName => "E-Commerce API";
}

Registration:

builder.Services.AddSingleton<
    IApplicationConfiguration,
    ApplicationConfiguration>();

Now multiple requests can use the same instance.

Request 1 ----+
              |
Request 2 ----+----> ApplicationConfiguration
              |
Request 3 ----+

11. Real-Time Example – Product Cache

Suppose an application frequently reads product categories.

Instead of querying the database repeatedly, we may maintain an application-level cache.

Conceptually:

public class ProductCache
{
    private readonly Dictionary<int, string> _products
        = new();

    public void Add(int id, string name)
    {
        _products[id] = name;
    }

    public string? Get(int id)
    {
        return _products.TryGetValue(id, out var name)
            ? name
            : null;
    }
}

Registration:

builder.Services.AddSingleton<ProductCache>();

The same cache object can be shared by requests.

However, when using mutable shared state, thread safety must be considered carefully. A singleton should not simply contain an ordinary mutable collection and assume concurrent requests are safe.


12. Complete E-Commerce Example

Let's build a simplified dependency chain.

OrdersController
       |
       v
OrderService
       |
       +---- OrderRepository
       |
       +---- PaymentService
       |
       +---- EmailService
       |
       +---- ApplicationCache
       |
       v
ApplicationDbContext

Possible registrations:

builder.Services.AddControllers();

builder.Services.AddScoped<IOrderService, OrderService>();

builder.Services.AddScoped<IOrderRepository, OrderRepository>();

builder.Services.AddScoped<IPaymentService, PaymentService>();

builder.Services.AddTransient<IEmailService, EmailService>();

builder.Services.AddSingleton<ProductCache>();

builder.Services.AddDbContext<ApplicationDbContext>();

Now each service has an appropriate lifetime based on its responsibilities.


13. What Happens During an HTTP Request?

Let's follow a request step-by-step.

Request:

POST /api/orders

Step 1 – Request Arrives

The client sends:

POST /api/orders

ASP.NET Core receives the request.


Step 2 – Request Scope Is Created

ASP.NET Core creates a scope for the HTTP request.

Request Scope
     |
     +-------------------------+
     |                         |
     v                         v
Scoped Services          Other Dependencies

Step 3 – Controller Is Created

ASP.NET Core needs:

OrdersController

Its constructor requires:

IOrderService

The DI container resolves it.


Step 4 – Scoped OrderService Is Created

Because it is registered as:

AddScoped<IOrderService, OrderService>()

ASP.NET Core creates one instance for this request.


Step 5 – Dependencies Are Resolved

Suppose OrderService requires:

IOrderRepository
IPaymentService
IEmailService

The DI container resolves them.

OrderService
     |
     +-- OrderRepository    Scoped
     |
     +-- PaymentService     Scoped
     |
     +-- EmailService       Transient

Step 6 – DbContext Is Resolved

OrderRepository requires:

ApplicationDbContext

Because DbContext is scoped, the request receives its scoped instance.

OrderService
      |
      v
OrderRepository
      |
      v
ApplicationDbContext

Step 7 – Singleton Is Resolved

Suppose ProductCache is needed.

The DI container checks:

Is ProductCache already created?

If yes:

Use existing instance

If no:

Create singleton instance

Step 8 – Request Executes

The order is processed.

Validate Order
      |
      v
Check Product
      |
      v
Create Order
      |
      v
Process Payment
      |
      v
Save Database Changes
      |
      v
Send Notification

Step 9 – Request Completes

The HTTP request ends.

ASP.NET Core disposes the request scope.

Scoped and transient disposable services associated with that scope are disposed according to the DI container's lifetime management.

The singleton remains alive while its container remains alive.


14. Visualizing All Three Lifetimes

Imagine three HTTP requests.

                  APPLICATION
                      |
             Singleton Instance
                      |
          +-----------+-----------+
          |           |           |
       Request 1   Request 2   Request 3
          |           |           |
       Scoped A    Scoped B    Scoped C
          |           |           |
       Transient   Transient   Transient
          A           B           C

The important point is:

Singleton
   |
   +-- Same instance across requests

Scoped
   |
   +-- Same instance within one request
   +-- Different instance for another request

Transient
   |
   +-- New instance whenever resolved

15. Important Difference: Scoped vs Transient

This is a common interview question.

Suppose:

builder.Services.AddScoped<IService, MyService>();

and the same service is requested twice within one scope.

Conceptually:

Request
  |
  +-- Resolve IService ---> Instance A
  |
  +-- Resolve IService ---> Instance A

Same instance.

With:

builder.Services.AddTransient<IService, MyService>();

you can get:

Request
  |
  +-- Resolve IService ---> Instance A
  |
  +-- Resolve IService ---> Instance B

Different instances.


16. Important Difference: Scoped vs Singleton

Scoped:

Request 1 ---> Instance A
Request 2 ---> Instance B
Request 3 ---> Instance C

Singleton:

Request 1 ---+
Request 2 ---+---> Same Instance A
Request 3 ---+

Therefore, singleton services should be designed very carefully because multiple requests can access the same instance concurrently.


17. Can Singleton Depend on Scoped Service?

This is an important interview question.

Suppose:

builder.Services.AddSingleton<MySingleton>();
builder.Services.AddScoped<MyScoped>();

and:

public class MySingleton
{
    private readonly MyScoped _scoped;

    public MySingleton(MyScoped scoped)
    {
        _scoped = scoped;
    }
}

This creates a lifetime mismatch.

A singleton lives much longer than a scoped service.

Conceptually:

Singleton
     |
     v
Scoped Service

The scoped service cannot naturally live for the entire lifetime of the singleton.

ASP.NET Core's DI validation can detect such captive dependency problems in appropriate environments/configurations.

General rule:

Singleton
   ↓
Should not directly depend on
   ↓
Scoped

18. Can Scoped Depend on Singleton?

Yes.

For example:

Scoped OrderService
        |
        v
Singleton ApplicationConfiguration

This is generally valid because the singleton has a lifetime longer than the scoped service.


19. Can Transient Depend on Scoped?

Within a valid request scope, yes.

For example:

Scoped OrderService
        |
        v
Transient EmailFormatter

The transient object is created for the resolution and can use dependencies available in that scope.


20. Can Singleton Depend on Transient?

This requires careful consideration.

Technically, a singleton can resolve a transient dependency during its construction, but that transient instance then effectively becomes held by the singleton for as long as the singleton holds it.

Therefore, the dependency's effective lifetime can become much longer than intended.

This is sometimes called a captive dependency.

So don't choose lifetimes merely because the DI container allows the registration.

Choose them based on the object's state and responsibilities.


21. Service Lifetime and Thread Safety

This is especially important for Singleton services.

Imagine:

public class CounterService
{
    private int _count;

    public void Increment()
    {
        _count++;
    }
}

If registered as:

builder.Services.AddSingleton<CounterService>();

multiple requests may access the same object concurrently.

Therefore:

Request A ----+
              |
Request B ----+----> Same Singleton
              |
Request C ----+

The implementation must be safe for concurrent access.

For shared mutable state, use appropriate thread-safe techniques or concurrency-safe collections where necessary.


22. Service Lifetime in Microservices

In a microservices architecture, each microservice normally has its own DI container and process/application lifetime.

For example:

Order Service
     |
     +-- Singleton
     +-- Scoped
     +-- Transient

Payment Service
     |
     +-- Singleton
     +-- Scoped
     +-- Transient

Inventory Service
     |
     +-- Singleton
     +-- Scoped
     +-- Transient

A singleton in the Order Service is not automatically shared with the Payment Service.

Each application has its own process/container and its own DI registrations.


23. Service Lifetime vs Database Lifetime

Don't confuse these concepts.

Service lifetime:

Transient
Scoped
Singleton

Database connection lifetime is a separate concern.

For example:

Web API
   |
   v
DbContext
   |
   v
Database Provider
   |
   v
Database

DbContext is normally scoped, while the underlying database connection management is handled by the database provider and connection pooling mechanisms.


24. Common Real-Time Registration

A typical ASP.NET Core application might contain:

builder.Services.AddControllers();

builder.Services.AddDbContext<ApplicationDbContext>();

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();

builder.Services.AddScoped<IPaymentService, PaymentService>();

builder.Services.AddTransient<IEmailTemplateService, EmailTemplateService>();

builder.Services.AddSingleton<IApplicationSettings, ApplicationSettings>();

A reasonable conceptual mapping is:

ServiceTypical LifetimeReason
DbContextScopedRequest/unit-of-work oriented
RepositoryScopedWorks with DbContext
Business ServiceScopedRequest-level operation
Stateless FormatterTransientLightweight/stateless
Application ConfigurationSingletonShared application-level data
In-memory CacheSingletonShared cache, if designed safely

These are common patterns, not absolute rules.


25. Service Life Cycle and IDisposable

Another important concept is disposal.

Suppose:

public class FileService : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Disposed");
    }
}

If the DI container creates and owns a disposable service, it generally manages its disposal according to the service lifetime and scope.

For example, a scoped disposable service is normally disposed when its request scope ends.

A singleton disposable service is normally disposed when the application's DI container is disposed.

This is one reason you should generally let the DI container manage dependencies that it creates rather than manually disposing injected dependencies.


26. Service Lifetime and Background Services

A common mistake is trying to inject a scoped service directly into a long-running BackgroundService.

For example:

public class OrderBackgroundService : BackgroundService
{
    private readonly ApplicationDbContext _context;

    public OrderBackgroundService(ApplicationDbContext context)
    {
        _context = context;
    }
}

This is problematic because BackgroundService is effectively long-lived, while DbContext is scoped.

A better approach is to create a scope when processing each unit of work:

public class OrderBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public OrderBackgroundService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        using var scope = _scopeFactory.CreateScope();

        var dbContext =
            scope.ServiceProvider
                 .GetRequiredService<ApplicationDbContext>();

        // Process work
    }
}

For repeated background work, create and dispose an appropriate scope for each unit of work rather than keeping one scoped dependency forever.


27. How DI Container Resolves a Service

Suppose:

builder.Services.AddScoped<IOrderService, OrderService>();

and:

public class OrderService : IOrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

When the controller requests IOrderService, the DI container performs roughly this process:

1. Controller requests IOrderService
             |
             v
2. DI Container checks registration
             |
             v
3. Finds OrderService
             |
             v
4. Checks OrderService lifetime
             |
             v
5. Scoped
             |
             v
6. Checks current scope
             |
             v
7. Creates OrderService if not already created
             |
             v
8. Sees IOrderRepository dependency
             |
             v
9. Resolves IOrderRepository
             |
             v
10. Creates OrderService
             |
             v
11. Injects dependencies
             |
             v
12. Controller receives OrderService

This is the essence of Dependency Injection.


28. Real-Time Request Example

Let's assume:

Customer places an order

Request:

POST /api/orders

Flow:

Client
  |
  | HTTP Request
  v
ASP.NET Core
  |
  v
Middleware Pipeline
  |
  v
Request Scope
  |
  v
OrdersController
  |
  v
OrderService
  |
  +---- OrderRepository
  |          |
  |          v
  |      DbContext
  |
  +---- PaymentService
  |
  +---- EmailService
  |
  +---- ProductCache
  |
  v
Database

Lifetimes might be:

OrderService        -> Scoped
OrderRepository     -> Scoped
DbContext           -> Scoped
PaymentService      -> Scoped
EmailService        -> Transient
ProductCache        -> Singleton

At the end:

HTTP Request Ends
       |
       v
Request Scope Disposed
       |
       +-- Scoped services disposed
       +-- Request-owned transient disposables disposed
       |
       v
Singleton remains available

29. Common Interview Questions

Q1. What are the three service lifetimes?

Answer:

Transient
Scoped
Singleton

Q2. What is Transient?

A new service instance is created each time the service is requested from the DI container.

services.AddTransient<IEmailService, EmailService>();

Q3. What is Scoped?

One service instance is generally created per scope. In ASP.NET Core Web API, this normally means one instance per HTTP request.

services.AddScoped<IOrderService, OrderService>();

Q4. What is Singleton?

One service instance is reused for the lifetime of the DI container/application.

services.AddSingleton<ICache, Cache>();

Q5. Which lifetime is normally used for DbContext?

DbContext is normally registered as Scoped in ASP.NET Core applications.


Q6. Why shouldn't DbContext normally be Singleton?

Because DbContext is designed around a unit-of-work pattern and is not intended to be shared concurrently across unrelated requests.


Q7. Which lifetime is best for stateless lightweight services?

Often Transient, although Scoped can also be appropriate depending on the service's dependencies and design.


Q8. Which lifetime is best for shared application-wide state?

A Singleton can be appropriate, but shared mutable state must be designed for concurrent access and application lifetime.


Q9. Can Singleton depend on Scoped?

Generally, no direct dependency should be created, because it creates a lifetime mismatch/captive dependency.


Q10. Can Scoped depend on Singleton?

Yes. This is generally valid.


30. Common Mistakes

Mistake 1 – Making Everything Singleton

Avoid:

services.AddSingleton<OrderService>();
services.AddSingleton<OrderRepository>();
services.AddSingleton<ApplicationDbContext>();

This can create serious lifetime and concurrency problems.


Mistake 2 – Making Everything Transient

Using transient everywhere can cause unnecessary object creation and can undermine intentional request-level sharing.


Mistake 3 – Ignoring Thread Safety

Singleton services may be accessed concurrently.

Never assume:

Singleton = Automatically Thread Safe

It is not.


Mistake 4 – Injecting Scoped Services into Long-Lived Services

For example:

BackgroundService
       |
       v
DbContext

Instead, create a scope when processing the background operation.


31. Easy Way to Remember

Remember this formula:

Transient = Every Time

Scoped = Every Request

Singleton = Entire Application

Or:

Transient
    ↓
New Object

Scoped
    ↓
One Object Per Scope

Singleton
    ↓
One Object Per Container Lifetime

32. Final Comparison

FeatureTransientScopedSingleton
New instance frequently?YesNoNo
Same instance within request?Not necessarilyYesYes
Same instance across requests?NoNoYes
Typical lifetimeResolutionScope/requestApplication/container
Thread safety concernUsually less shared stateUsually less shared across requestsHigh if mutable
DbContext
RepositoryUsually Scoped
Stateless lightweight serviceSometimesSometimes
Application-wide cache✅, if designed safely
Configuration serviceSometimesSometimesOften

33. Complete Mental Model

When you see:

builder.Services.AddTransient<A>();
builder.Services.AddScoped<B>();
builder.Services.AddSingleton<C>();

think:

                    DI CONTAINER
                         |
        +----------------+----------------+
        |                |                |
        v                v                v
    TRANSIENT         SCOPED          SINGLETON
        |                |                |
     New Object       One per Scope    One per Container
        |                |                |
        |           HTTP Request       Application
        |                |                |
        v                v                v
     A1, A2...          B1              C1
                       B2              C1
                       B3              C1

The key idea is that service lifetime is not merely about object creation—it determines how long the object can retain state and who can share that state.


Conclusion

Service Lifetime is a fundamental part of Dependency Injection in ASP.NET Core.

The three primary lifetimes are:

Transient → New instance when resolved

Scoped → One instance per scope/request

Singleton → One instance for the DI container lifetime

For a real-world e-commerce Web API, a common design is:

Controller
    |
    v
OrderService        → Scoped
    |
    +---- Repository → Scoped
    |
    +---- DbContext  → Scoped
    |
    +---- Payment    → Scoped
    |
    +---- Email      → Transient
    |
    +---- Cache      → Singleton

Choosing the correct lifetime is important for:

  • Performance

  • Memory management

  • Thread safety

  • Database consistency

  • Resource management

  • Scalability

  • Application stability

The most important interview rule to remember is:

Transient = new instance, Scoped = one instance per scope/request, Singleton = one instance for the container lifetime.

Once you understand this concept, Dependency Injection, DbContext, middleware, background services, repositories, caching, and ASP.NET Core application architecture become much easier to understand.

JWT Authentication & Authorization — Complete Flow


Imagine the application has:

  • Angular / React frontend

  • ASP.NET Core Web API

  • SQL Server database

  • JWT-based authentication

The overall flow is:

User
  |
  | 1. Login: username + password
  v
Frontend
  |
  | 2. POST /api/auth/login
  v
Web API
  |
  | 3. Validate credentials
  v
Database
  |
  | 4. User is valid
  v
Web API
  |
  | 5. Create JWT
  v
Frontend
  |
  | 6. Store JWT
  |
  | 7. Send JWT in Authorization Header
  v
Web API
  |
  | 8. Validate JWT
  |
  | 9. Authentication
  |
  | 10. Authorization
  v
Controller
  |
  v
Response

Now let's understand every step.


1. What problem does JWT solve?

Suppose a user logs into your application.

Username: mahesh
Password: ********

The API verifies the username and password.

But after login, the API needs to know:

"Who is making this next request?"

For example:

GET /api/orders

The API needs to know:

Which user?
Is the user authenticated?
What roles does the user have?
Is the user allowed to access orders?

JWT provides a way for the client to prove its identity on subsequent requests.


2. Where does the JWT flow start?

The JWT authentication flow starts when the user performs login.

For example:

POST /api/auth/login

Request:

{
    "username": "mahesh",
    "password": "Password123"
}

The request reaches the Authentication API.


3. Step 1 — User sends credentials

The frontend sends:

Username
Password

to:

POST /api/auth/login

For example:

Angular Application
       |
       | username + password
       v
ASP.NET Core Web API

The password should be transmitted over HTTPS, not plain HTTP.


4. Step 2 — API validates the user

The API receives the credentials.

It queries the database:

SELECT Id, UserName, PasswordHash, Role
FROM Users
WHERE UserName = 'mahesh'

The application should compare the supplied password against the stored password hash.

It should not store plain-text passwords.

If authentication fails:

401 Unauthorized

If authentication succeeds:

UserId       = 101
Username     = mahesh
Role         = Customer

Now the API can create a JWT.


5. Step 3 — JWT is created

A JWT normally looks like this:

xxxxx.yyyyy.zzzzz

There are three parts:

HEADER.PAYLOAD.SIGNATURE

For example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMDEiLCJyb2xlIjoiQ3VzdG9tZXIifQ
.
abc123xyz...

These three parts have different responsibilities.


6. JWT Header

The header contains information about the token.

Example:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg

This tells the receiver which signing algorithm is being used.

For example:

HS256

or:

RS256

typ

This tells us the token type:

JWT

Conceptually:

HEADER
   |
   +-- Algorithm
   |
   +-- Token Type

7. JWT Payload

The payload contains claims.

For example:

{
  "sub": "101",
  "name": "Mahesh",
  "role": "Customer",
  "email": "mahesh@example.com",
  "exp": 1787220000
}

These are called claims.

Common claims include:

ClaimMeaning
subSubject/User ID
nameUser name
emailEmail
roleUser role
issToken issuer
audIntended audience
iatIssued-at time
expExpiration time

For example:

{
   "sub": "101",
   "role": "Admin"
}

means:

User ID = 101
Role = Admin

Important security point

The JWT payload is encoded, not encrypted, in a normal JWT.

Therefore, don't put sensitive information such as:

Password
Credit card number
Secret keys

inside the payload.


8. JWT Signature

This is the most important part for understanding JWT security.

Conceptually, the server takes:

Base64Url(Header)
+
"."
+
Base64Url(Payload)

and signs that data using a secret/private key.

For example, conceptually with HMAC:

Signature =
HMACSHA256(
    Base64Url(Header) + "." +
    Base64Url(Payload),
    SecretKey
)

The result becomes:

HEADER.PAYLOAD.SIGNATURE

9. Why do we need the Signature?

Imagine the original payload is:

{
    "userId": 101,
    "role": "Customer"
}

An attacker might try to change it to:

{
    "userId": 101,
    "role": "Admin"
}

But the attacker doesn't have the signing secret/private key.

Therefore, they cannot generate a valid signature for the modified payload.

When the API receives the token, it validates the signature.

If the token was modified:

Payload changed
      ↓
Signature no longer matches
      ↓
JWT validation fails
      ↓
401 Unauthorized

So the signature provides integrity/authenticity of the token, assuming the signing key is properly protected.


10. JWT is returned to the client

After successful login:

Web API
   |
   | JWT
   v
Frontend

For example:

{
    "accessToken": "eyJhbGciOiJIUzI1NiIs..."
}

Now the frontend has the access token.


11. What happens on the next API request?

Suppose the user wants to see orders.

Frontend sends:

GET /api/orders

But how does the API know who the user is?

The frontend sends the JWT using the HTTP Authorization header.

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

This is extremely important.


12. What is the Authorization Header?

The HTTP request looks like:

GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

There are two important pieces:

Authorization
      |
      +-- Bearer
      |
      +-- JWT Token

Bearer essentially means:

"The caller is presenting this access token as its credential."


13. Complete request flow

Now we can visualize the entire process:

                    LOGIN
                      |
                      v
              +---------------+
              |    Frontend   |
              +---------------+
                      |
                      | username/password
                      v
              +---------------+
              |   Auth API    |
              +---------------+
                      |
                      | Validate credentials
                      v
              +---------------+
              |   Database    |
              +---------------+
                      |
                      | User valid
                      v
              +---------------+
              |   Auth API    |
              +---------------+
                      |
                      | Create JWT
                      v
          +-------------------------+
          | HEADER.PAYLOAD.SIGNATURE|
          +-------------------------+
                      |
                      | JWT
                      v
              +---------------+
              |    Frontend   |
              +---------------+

Then:

                API REQUEST
                     |
                     v
              +-------------+
              |  Frontend   |
              +-------------+
                     |
                     | Authorization:
                     | Bearer JWT
                     v
              +-------------+
              |  Web API    |
              +-------------+
                     |
                     v
              JWT Middleware
                     |
             +-------+-------+
             |               |
          Invalid           Valid
             |               |
             v               v
           401          User.Identity
                           created
                              |
                              v
                       Authorization
                              |
                    +---------+---------+
                    |                   |
                 Allowed              Denied
                    |                   |
                    v                   v
                Controller             403

14. What happens inside ASP.NET Core?

Suppose we have:

[Authorize]
[HttpGet("orders")]
public IActionResult GetOrders()
{
    return Ok();
}

The request arrives:

GET /api/orders
Authorization: Bearer <JWT>

ASP.NET Core's JWT authentication middleware processes the token before the controller action executes.

Conceptually:

HTTP Request
     |
     v
ASP.NET Core Middleware
     |
     v
JWT Authentication Handler
     |
     v
Read Authorization Header
     |
     v
Extract Bearer Token
     |
     v
Validate JWT
     |
     v
Create ClaimsPrincipal
     |
     v
Authorization
     |
     v
Controller

15. JWT Validation

The API validates several things depending on its configuration.

For example:

Signature

Is the signature valid?

Issuer

Who issued this token?

Example:

https://my-auth-server

Audience

Is this token intended for my API?

Example:

my-ecommerce-api

Expiration

Has the token expired?

For example:

{
    "exp": 1787220000
}

If the current time is beyond the expiration time, the token is rejected.


16. Authentication vs Authorization

This is one of the most important interview questions.

Authentication

Authentication answers:

Who are you?

Example:

User logs in
     ↓
Username/password validated
     ↓
JWT issued
     ↓
JWT presented to API
     ↓
API validates JWT
     ↓
User is authenticated

Authentication establishes the user's identity.


17. Authorization

Authorization answers:

What are you allowed to do?

Suppose we have:

Admin
Customer
Manager

JWT:

{
    "sub": "101",
    "role": "Customer"
}

Then:

[Authorize]

means:

An authenticated user can access this endpoint.

But:

[Authorize(Roles = "Admin")]

means:

Only authenticated users with the Admin role can access this endpoint.

If a Customer calls it:

Authenticated? YES

Authorized? NO

Result:
403 Forbidden

18. 401 vs 403

This is another important interview question.

401 Unauthorized

Usually means:

Authentication failed / no valid authentication

Examples:

No token
Invalid token
Expired token
Invalid signature

Conceptually:

Who are you?
→ I can't authenticate you.

403 Forbidden

Means:

You are authenticated,
but you don't have permission.

Example:

User = Customer

Endpoint requires = Admin

Result:

403 Forbidden

Think:

401 = I don't know who you are.

403 = I know who you are,
      but you're not allowed.

19. Authentication + Authorization Example

Suppose:

[Authorize]
[HttpGet("profile")]
public IActionResult Profile()
{
    return Ok();
}

Any authenticated user can access it.

But:

[Authorize(Roles = "Admin")]
[HttpDelete("users/{id}")]
public IActionResult DeleteUser(int id)
{
    return Ok();
}

Only Admin users can access it.

The flow becomes:

JWT
 |
 v
Signature validation
 |
 v
Token valid?
 |
 +---- NO ----> 401
 |
 YES
 |
 v
Authentication successful
 |
 v
Read Claims
 |
 v
Role = Customer?
 |
 v
Endpoint requires Admin
 |
 v
Authorization fails
 |
 v
403 Forbidden

20. Where does the Role come from?

The role can be included as a JWT claim.

Example:

{
    "sub": "101",
    "name": "Mahesh",
    "role": "Admin"
}

ASP.NET Core converts JWT claims into a ClaimsPrincipal.

You can then access claims:

var userId = User.FindFirst("sub")?.Value;

or:

var role = User.FindFirst("role")?.Value;

Depending on configuration, role claims can also be accessed through:

User.IsInRole("Admin")

21. How does ASP.NET Core know which JWT to validate?

You configure JWT authentication in the application.

Conceptually:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,

            ValidIssuer = "...",
            ValidAudience = "...",
            IssuerSigningKey = ...
        };
    });

And:

app.UseAuthentication();
app.UseAuthorization();

The order is important:

UseAuthentication()
        ↓
UseAuthorization()
        ↓
MapControllers()

Authentication must establish the user's identity before authorization makes the access decision.


22. Why is the Header important?

There are actually two places where "header" can mean different things.

JWT Header

Inside the JWT:

{
   "alg": "HS256",
   "typ": "JWT"
}

This describes the token.

HTTP Authorization Header

Outside the JWT:

Authorization: Bearer <token>

This transports the JWT from the client to the API.

So:

JWT Header
     ↓
Describes JWT

HTTP Authorization Header
     ↓
Carries JWT to API

Don't confuse these two.


23. Complete E-Commerce Example

Let's imagine an e-commerce application.

User:

Mahesh
UserId = 101
Role = Customer

Step 1 — Login

POST /api/auth/login
{
   "username": "mahesh",
   "password": "********"
}

Step 2 — Database validation

Username exists?
        ↓
Password valid?
        ↓
Role = Customer
        ↓
YES

Step 3 — JWT creation

Payload:

{
   "sub": "101",
   "name": "Mahesh",
   "role": "Customer",
   "exp": "..."
}

JWT:

HEADER.PAYLOAD.SIGNATURE

Step 4 — JWT returned

Authentication API
       ↓
      JWT
       ↓
    Frontend

Step 5 — Get orders

GET /api/orders
Authorization: Bearer <JWT>

Step 6 — API validates JWT

Token exists?
     ↓
Signature valid?
     ↓
Issuer valid?
     ↓
Audience valid?
     ↓
Token expired?
     ↓
All valid

Step 7 — Authentication

User = Mahesh
UserId = 101
Role = Customer

Step 8 — Authorization

Suppose:

[Authorize]

Customer is allowed.

Therefore:

Controller executes

24. What if the JWT is modified?

Original:

{
   "userId": 101,
   "role": "Customer"
}

Attacker changes it:

{
   "userId": 101,
   "role": "Admin"
}

The attacker doesn't have the signing key.

Therefore:

Modified Payload
       ↓
Signature doesn't match
       ↓
JWT validation fails
       ↓
Authentication fails
       ↓
401

This is why the signature is critical.


25. What if the JWT is expired?

Suppose:

{
   "sub": "101",
   "exp": 1787220000
}

The API checks:

Current time > exp?

If yes:

Token expired
     ↓
Authentication fails
     ↓
401 Unauthorized

The frontend can then obtain a new access token using an appropriate token renewal mechanism, such as a refresh-token flow, depending on the authentication architecture.


26. Does every API call query the User table?

Not necessarily.

That's one of the major benefits of JWT.

For a properly configured self-contained JWT, the API can validate:

Signature
Issuer
Audience
Expiration
Claims

without querying the user database on every request.

For example:

Request
   ↓
JWT
   ↓
Signature validation
   ↓
Claims
   ↓
Authorization
   ↓
Controller

However, applications sometimes still consult a database/cache for things such as account status, revocation, permissions that must change immediately, or other business rules.


27. Where is the JWT stored?

This depends on the frontend architecture and security requirements.

A common browser approach is to use secure cookie-based mechanisms, especially when designed to mitigate token theft/XSS risks.

Another approach is storing an access token in browser storage, but storing long-lived authentication tokens in localStorage has important security trade-offs because JavaScript can access it.

For a production system, token storage should be designed together with:

HTTPS
XSS protection
CSRF protection
Token lifetime
Refresh-token strategy
Cookie settings

28. JWT Flow — Start to End

Here's the complete flow you can remember for interviews:

                 ┌──────────────┐
                 │     USER     │
                 └──────┬───────┘
                        │
                        │ Login
                        ▼
                 ┌──────────────┐
                 │   FRONTEND   │
                 └──────┬───────┘
                        │
                        │ username/password
                        ▼
                 ┌──────────────┐
                 │  AUTH API    │
                 └──────┬───────┘
                        │
                        │ Validate
                        ▼
                 ┌──────────────┐
                 │   DATABASE   │
                 └──────┬───────┘
                        │
                        │ Valid
                        ▼
                 ┌──────────────┐
                 │  JWT CREATE  │
                 └──────┬───────┘
                        │
                        │
                HEADER.PAYLOAD
                  .SIGNATURE
                        │
                        ▼
                 ┌──────────────┐
                 │   FRONTEND   │
                 └──────┬───────┘
                        │
                        │ Authorization:
                        │ Bearer JWT
                        ▼
                 ┌──────────────┐
                 │   WEB API    │
                 └──────┬───────┘
                        │
                        ▼
                JWT VALIDATION
                        │
             ┌──────────┴──────────┐
             │                     │
          Invalid                 Valid
             │                     │
             ▼                     ▼
            401              Authentication
                                  │
                                  ▼
                            Authorization
                                  │
                    ┌─────────────┴────────────┐
                    │                          │
                 Allowed                    Denied
                    │                          │
                    ▼                          ▼
               Controller                    403
                    │
                    ▼
                 Response

29. The most important distinction

Remember these three concepts:

Header

What algorithm/type is this JWT using?

Payload

Who is the user?
What claims/attributes are associated with the token?

Signature

Has the token been altered,
and can it be validated using the expected signing key?

And remember:

HTTP Authorization Header
        ↓
Carries JWT

JWT Header
        ↓
Describes JWT

JWT Payload
        ↓
Contains Claims

JWT Signature
        ↓
Protects token integrity/authenticity

30. One-line interview answer

If an interviewer asks:

"Explain JWT authentication flow."

A strong answer is:

"When a user logs in, the authentication service validates the credentials and creates a signed JWT containing claims such as user ID, issuer, audience, role and expiration. The client then sends that token with subsequent API requests in the HTTP Authorization header using the Bearer scheme. ASP.NET Core's JWT authentication middleware extracts and validates the token's signature, issuer, audience and lifetime and creates the authenticated ClaimsPrincipal. The authorization middleware then evaluates policies or roles, such as [Authorize(Roles = "Admin")]. If authentication fails, the API returns 401; if authentication succeeds but authorization fails, it returns 403. If both succeed, the request reaches the controller."

That is the complete JWT authentication → authorization flow from login to API response.

Thursday, August 20, 2026

Saga Design Pattern – Complete Guide with E-Commerce Example

1. What Problem Does Saga Solve?

Imagine an e-commerce application with these microservices:

                    E-Commerce Application
                            |
        +-------------------+-------------------+
        |                   |                   |
   Order Service       Payment Service     Inventory Service
        |                   |                   |
        +-------------------+-------------------+
                            |
                    Shipping Service

A customer places an order.

The business process might be:

Create Order
    ↓
Reserve Inventory
    ↓
Process Payment
    ↓
Create Shipment
    ↓
Order Completed

The problem is that each operation belongs to a different database.

For example:

Order Service       → OrderDB
Inventory Service   → InventoryDB
Payment Service     → PaymentDB
Shipping Service    → ShippingDB

We cannot normally use a single SQL transaction such as:

BEGIN TRANSACTION

OrderDB
InventoryDB
PaymentDB
ShippingDB

COMMIT

because these are independent microservices.

This is where Saga Pattern comes in.


2. What Is Saga Design Pattern?

A Saga is a sequence of local transactions where each microservice performs its own transaction.

If one transaction fails, previously completed transactions are compensated by executing corresponding compensating transactions.

Conceptually:

Transaction 1
     ↓
Transaction 2
     ↓
Transaction 3
     ↓
Transaction 4

If Transaction 3 fails:

Transaction 1 ✓
Transaction 2 ✓
Transaction 3 ✗

        ↓

Compensation 2
        ↓
Compensation 1

So instead of a traditional distributed ACID transaction, Saga provides eventual consistency using local transactions + compensation.


3. E-Commerce Example

Suppose customer places an order:

Order #1001

Product: Laptop
Quantity: 1
Price: ₹80,000

The workflow is:

Customer
   |
   ↓
Order Service
   |
   ↓
Inventory Service
   |
   ↓
Payment Service
   |
   ↓
Shipping Service
   |
   ↓
Order Completed

Let's define the transactions.

T1 – Create Order

Order Service:

Order Status = Pending

T2 – Reserve Inventory

Inventory Service:

Laptop Stock
100 → 99

T3 – Process Payment

Payment Service:

₹80,000 charged

T4 – Create Shipment

Shipping Service:

Shipment Created

Finally:

Order Status = Confirmed

4. What Happens If Payment Fails?

Suppose:

T1 Create Order        ✓
T2 Reserve Inventory   ✓
T3 Payment             ✗

We cannot simply rollback T1 and T2 using a normal database rollback because they happened in different databases.

Instead:

Payment Failed
      ↓
Release Inventory
      ↓
Cancel Order

So:

T1 Create Order ✓
       ↓
T2 Reserve Stock ✓
       ↓
T3 Payment ✗
       ↓
C2 Release Stock ✓
       ↓
C1 Cancel Order ✓

This is the core concept of Saga.


5. Saga Has Two Main Approaches

There are two major implementations.

Approach 1 – Choreography

Services communicate through events.

Order Service
     |
 OrderCreated
     ↓
Inventory Service
     |
InventoryReserved
     ↓
Payment Service
     |
PaymentCompleted
     ↓
Shipping Service

There is no central coordinator.


Approach 2 – Orchestration

A central Saga Orchestrator controls the workflow.

                 Saga Orchestrator
                        |
          +-------------+-------------+
          |             |             |
          ↓             ↓             ↓
      Order          Inventory      Payment
      Service         Service       Service
                                      |
                                      ↓
                                  Shipping

The orchestrator says:

Reserve inventory

then:

Process payment

then:

Create shipment

If something fails:

Release inventory
Cancel order

For an enterprise e-commerce application, orchestration is often easier to understand and manage, especially when the workflow becomes complex.


6. Which One Should We Use?

FeatureChoreographyOrchestration
Central controllerNoYes
Simple workflowsExcellentGood
Complex workflowsDifficultExcellent
DebuggingDifficultEasier
Business workflow visibilityLowerHigher
CouplingEvent-basedOrchestrator-based
Failure handlingDistributedCentralized
Large enterprise workflowsCan become complicatedOften preferable

For the example below, I'll use Saga Orchestration.


7. Overall Architecture

Let's design the system.

                         Client
                           |
                           ↓
                    API Gateway
                           |
                           ↓
                    Order Service
                           |
                           ↓
                  Saga Orchestrator
                           |
          +----------------+----------------+
          |                |                |
          ↓                ↓                ↓
     Inventory          Payment          Shipping
      Service           Service           Service
          |                |                |
      InventoryDB       PaymentDB       ShippingDB

Communication could use:

Azure Service Bus
Kafka
RabbitMQ

For an Azure-based .NET system, Azure Service Bus is a natural choice.


8. Database Design

An important Saga principle is:

Each microservice owns its own database.

Don't do this:

Order Service
       |
       ↓
Shared Database
       ↑
       |
Inventory Service

Instead:

Order Service
     ↓
OrderDB

Inventory Service
     ↓
InventoryDB

Payment Service
     ↓
PaymentDB

Shipping Service
     ↓
ShippingDB

9. Order Model

Order Service might have:

public class Order
{
    public Guid Id { get; set; }

    public Guid CustomerId { get; set; }

    public decimal TotalAmount { get; set; }

    public OrderStatus Status { get; set; }

    public DateTime CreatedAt { get; set; }
}

Status:

public enum OrderStatus
{
    Pending,
    InventoryReserved,
    PaymentProcessing,
    Confirmed,
    Failed,
    Cancelled
}

10. Order Items

public class OrderItem
{
    public Guid Id { get; set; }

    public Guid OrderId { get; set; }

    public Guid ProductId { get; set; }

    public int Quantity { get; set; }

    public decimal Price { get; set; }
}

11. Inventory Model

Inventory Service owns:

public class Inventory
{
    public Guid ProductId { get; set; }

    public int AvailableQuantity { get; set; }

    public int ReservedQuantity { get; set; }
}

Example:

Product       Available     Reserved

Laptop          100            0

After reservation:

Laptop           99            1

12. Inventory Reservation Model

We should maintain a separate reservation record.

public class InventoryReservation
{
    public Guid Id { get; set; }

    public Guid OrderId { get; set; }

    public Guid ProductId { get; set; }

    public int Quantity { get; set; }

    public ReservationStatus Status { get; set; }
}

Status:

public enum ReservationStatus
{
    Reserved,
    Released
}

Why?

Because Saga requires us to know:

What exactly should I compensate?


13. Payment Model

Payment Service:

public class Payment
{
    public Guid Id { get; set; }

    public Guid OrderId { get; set; }

    public decimal Amount { get; set; }

    public PaymentStatus Status { get; set; }

    public string TransactionReference { get; set; }
}

Status:

public enum PaymentStatus
{
    Pending,
    Completed,
    Failed,
    Refunded
}

14. Shipment Model

Shipping Service:

public class Shipment
{
    public Guid Id { get; set; }

    public Guid OrderId { get; set; }

    public string Address { get; set; }

    public ShipmentStatus Status { get; set; }
}

15. Saga State Model

The orchestrator should maintain Saga state.

For example:

public class OrderSaga
{
    public Guid SagaId { get; set; }

    public Guid OrderId { get; set; }

    public SagaStatus Status { get; set; }

    public bool OrderCreated { get; set; }

    public bool InventoryReserved { get; set; }

    public bool PaymentCompleted { get; set; }

    public bool ShipmentCreated { get; set; }

    public DateTime CreatedAt { get; set; }

    public DateTime UpdatedAt { get; set; }
}

This becomes very useful for:

  • monitoring

  • retries

  • recovery

  • debugging

  • compensation


16. Complete Saga Flow

Let's look at the complete process.

Customer
   |
   | Place Order
   ↓
Order Service
   |
   | Order Created
   ↓
Saga Orchestrator
   |
   | Reserve Inventory
   ↓
Inventory Service
   |
   | Inventory Reserved
   ↓
Saga Orchestrator
   |
   | Process Payment
   ↓
Payment Service
   |
   | Payment Completed
   ↓
Saga Orchestrator
   |
   | Create Shipment
   ↓
Shipping Service
   |
   | Shipment Created
   ↓
Saga Orchestrator
   |
   ↓
Order Confirmed

17. Step 1 – Customer Creates Order

Client:

POST /api/orders

Request:

{
  "customerId": "C001",
  "items": [
    {
      "productId": "P100",
      "quantity": 1
    }
  ]
}

Order Service creates:

OrderId = 1001
Status = Pending

Database:

Orders

1001 | C001 | 80000 | Pending

Then publish:

OrderCreated

Event:

public record OrderCreatedEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal Amount);

18. Step 2 – Saga Starts

The orchestrator receives:

OrderCreated

It creates:

SagaId = S1001
OrderId = 1001
Status = Started

Then sends:

ReserveInventory

19. Step 3 – Inventory Reservation

Inventory Service receives:

{
  "sagaId": "S1001",
  "orderId": "1001",
  "productId": "P100",
  "quantity": 1
}

It executes a local database transaction.

For example:

BEGIN TRANSACTION

Check available stock

Available = Available - 1

Create Reservation

COMMIT

Database becomes:

Available = 99
Reserved = 1

Then publish:

InventoryReserved

20. Step 4 – Payment

Saga orchestrator receives:

InventoryReserved

Then sends:

ProcessPayment

Payment Service:

BEGIN TRANSACTION

Create Payment
Status = Processing

Call payment provider

Payment successful

Status = Completed

COMMIT

Then:

PaymentCompleted

21. Step 5 – Shipping

Saga receives:

PaymentCompleted

Then:

CreateShipment

Shipping Service creates:

ShipmentId = SH1001
OrderId = 1001
Status = Created

Then publishes:

ShipmentCreated

22. Step 6 – Complete Saga

Saga Orchestrator receives:

ShipmentCreated

Now:

OrderCreated       ✓
InventoryReserved  ✓
PaymentCompleted   ✓
ShipmentCreated    ✓

So:

Saga Status = Completed

And Order Service is instructed:

ConfirmOrder

Order:

1001 | Confirmed

23. What Happens When Payment Fails?

This is where Saga becomes interesting.

Suppose:

Order Created       ✓
Inventory Reserved  ✓
Payment             ✗

The orchestrator receives:

PaymentFailed

It knows:

InventoryReserved = true
PaymentCompleted = false

Therefore compensation is required.


24. Compensation Flow

The orchestrator sends:

ReleaseInventory

Inventory Service:

BEGIN TRANSACTION

Available = Available + 1

Reservation.Status = Released

COMMIT

Now:

Available = 100
Reserved = 0

Then orchestrator sends:

CancelOrder

Order Service:

Order.Status = Cancelled

Final state:

Order       = Cancelled
Inventory   = Released
Payment     = Failed
Saga        = Compensated

25. Important Point – Compensation Is NOT Rollback

This is one of the most important interview concepts.

Traditional transaction:

BEGIN

Operation A
Operation B
Operation C

ROLLBACK

Saga:

Operation A
Operation B
Operation C → FAILED

Compensation B
Compensation A

There is no global database rollback.

Instead:

A compensating transaction semantically reverses the business effect of a previous transaction.


26. Example: Payment Succeeds but Shipping Fails

Consider:

Order        ✓
Inventory    ✓
Payment      ✓
Shipping     ✗

Now we need to compensate.

Possible sequence:

Shipping Failed
       ↓
Refund Payment
       ↓
Release Inventory
       ↓
Cancel Order

So:

T1 Create Order       ✓
T2 Reserve Inventory  ✓
T3 Payment            ✓
T4 Shipping           ✗

C3 Refund Payment     ✓
C2 Release Inventory  ✓
C1 Cancel Order       ✓

Final:

Order = Cancelled
Inventory = Released
Payment = Refunded
Shipping = Failed

27. What If Compensation Also Fails?

This is a very important real-world scenario.

Suppose:

Payment Failed
      ↓
Release Inventory
      ↓
Inventory Service FAILED

Now:

Order = Pending
Inventory = Reserved
Payment = Failed

We cannot simply give up.

The Saga must retry the compensation.

ReleaseInventory
      ↓
FAILED
      ↓
Retry
      ↓
FAILED
      ↓
Retry
      ↓
SUCCESS

Therefore Saga implementations need:

  • Retry

  • Dead-letter queue

  • Idempotency

  • Timeout

  • Monitoring

  • Manual recovery


28. Retry Strategy

For example:

Attempt 1
   ↓
5 seconds
   ↓
Attempt 2
   ↓
30 seconds
   ↓
Attempt 3
   ↓
5 minutes

This is called exponential backoff.

For Azure Service Bus, failed messages can eventually be moved to a dead-letter queue.


29. Idempotency Is Extremely Important

Suppose:

ReserveInventory

message is delivered twice.

Without idempotency:

Message 1 → Reserve 1 item
Message 2 → Reserve another item

Incorrect:

Stock: 100 → 98

But we wanted:

Stock: 100 → 99

Therefore every command should have a unique identifier.

Example:

public class ProcessedMessage
{
    public Guid MessageId { get; set; }

    public DateTime ProcessedAt { get; set; }
}

Before processing:

Has MessageId already been processed?

If yes:

Ignore

Otherwise:

Process
Save MessageId

30. Better Idempotency Model

Instead of only MessageId, use a business operation ID.

Example:

SagaId = S1001
OrderId = 1001
Operation = ReserveInventory

Create unique constraint:

(SagaId, Operation)

Then duplicate commands cannot create duplicate reservations.


31. Transactional Outbox Pattern

There is another major problem.

Suppose Order Service does:

BEGIN TRANSACTION

Insert Order

COMMIT

Then:

Publish OrderCreated

What if the application crashes between these operations?

Database Insert ✓
Publish Event ✗

Now the order exists but Saga never receives the event.

This is where Transactional Outbox Pattern is commonly combined with Saga.


32. Outbox Table

Order Service database:

Orders
OutboxMessages

When creating the order:

BEGIN TRANSACTION

INSERT INTO Orders

INSERT INTO OutboxMessages

COMMIT

Both happen in the same local database transaction.

Example:

Orders

OrderId = 1001
Status = Pending

And:

OutboxMessages

MessageId = M1001
Type = OrderCreated
Payload = {...}
Published = false

A background publisher then reads:

Published = false

and sends the message.

After successful publishing:

Published = true

This greatly improves reliability.


33. Saga + Outbox Architecture

A robust architecture becomes:

                         Saga Orchestrator
                                |
                         Message Broker
                                |
          +---------------------+---------------------+
          |                     |                     |
          ↓                     ↓                     ↓
      Order Service        Inventory Service      Payment
          |                     |                     |
       OrderDB              InventoryDB           PaymentDB
          |                     |                     |
       Outbox                 Outbox                Outbox
          |                     |                     |
          +---------------------+---------------------+
                                |
                         Azure Service Bus

34. Commands vs Events

This distinction is important.

Command

A command tells another service:

Do something.

Examples:

CreateOrder
ReserveInventory
ProcessPayment
CreateShipment
RefundPayment
ReleaseInventory
CancelOrder

Event

An event says:

Something happened.

Examples:

OrderCreated
InventoryReserved
InventoryReservationFailed
PaymentCompleted
PaymentFailed
ShipmentCreated
ShipmentFailed

35. Example Command

public record ReserveInventoryCommand(
    Guid MessageId,
    Guid SagaId,
    Guid OrderId,
    Guid ProductId,
    int Quantity);

36. Example Event

public record InventoryReservedEvent(
    Guid MessageId,
    Guid SagaId,
    Guid OrderId);

Failure:

public record InventoryReservationFailedEvent(
    Guid MessageId,
    Guid SagaId,
    Guid OrderId,
    string Reason);

37. Saga State Machine

The orchestrator can be modeled as a state machine.

             OrderCreated
                  |
                  ↓
          InventoryPending
                  |
        +---------+---------+
        |                   |
     Success              Failure
        |                   |
        ↓                   ↓
 PaymentPending        CancelOrder
        |
   +----+----+
   |         |
Success     Failure
   |         |
   ↓         ↓
Shipping   ReleaseInventory
Pending       |
   |          ↓
Success    CancelOrder
   |
   ↓
Completed

This is a very good way to explain Saga in an interview.


38. Orchestrator Pseudocode

Conceptually:

public async Task Handle(OrderCreatedEvent message)
{
    await ReserveInventory(message);
}

When inventory succeeds:

public async Task Handle(InventoryReservedEvent message)
{
    await ProcessPayment(message);
}

Payment succeeds:

public async Task Handle(PaymentCompletedEvent message)
{
    await CreateShipment(message);
}

Shipment succeeds:

public async Task Handle(ShipmentCreatedEvent message)
{
    await ConfirmOrder(message);
}

Payment fails:

public async Task Handle(PaymentFailedEvent message)
{
    await ReleaseInventory(message);
    await CancelOrder(message);
}

39. Compensation Table

A useful way to design a Saga is to create a compensation matrix.

Forward TransactionCompensation
Create OrderCancel Order
Reserve InventoryRelease Inventory
Process PaymentRefund Payment
Create ShipmentCancel Shipment
Apply CouponRestore Coupon
Allocate Loyalty PointsReturn Loyalty Points

For every business transaction, ask:

If this succeeds and something later fails, how do I undo its business effect?

If you cannot answer that, your Saga design isn't complete.


40. Data Consistency

A common question is:

How does Saga maintain data consistency?

It does not provide immediate strong consistency across all databases like a single ACID transaction.

Instead it provides:

Eventual Consistency

For example:

Initially:

Order = Pending
Inventory = Reserved
Payment = Processing

After processing:

Order = Confirmed
Inventory = Reserved
Payment = Completed

Or if failure occurs:

Order = Cancelled
Inventory = Released
Payment = Failed

The system may temporarily have intermediate states, but eventually it reaches a valid business state.


41. Important: Don't Compensate Everything Blindly

Suppose:

Inventory reserved
Payment completed
Shipping failed

You shouldn't simply execute compensation commands without knowing the actual state.

Maintain Saga state:

InventoryReserved = true
PaymentCompleted = true
ShipmentCreated = false

Then compensation is based on completed steps.

if PaymentCompleted
    RefundPayment()

if InventoryReserved
    ReleaseInventory()

if OrderCreated
    CancelOrder()

42. Timeout Handling

Suppose Payment Service doesn't respond.

Payment Request
      ↓
Waiting...
      ↓
Waiting...
      ↓
Timeout

The orchestrator should not wait forever.

For example:

Payment timeout = 5 minutes

Then:

Payment Timeout
      ↓
Check payment status
      ↓
If unknown → retry/query provider
      ↓
If definitely failed → compensate

This is especially important with external payment gateways.


43. Why Payment Status Needs Special Care

Imagine:

Payment Service → Payment Gateway

Payment request is sent.

Gateway processes it.

But response is lost.

Your service sees:

Timeout

You must not automatically refund or retry blindly because the first payment might actually have succeeded.

You may accidentally charge the customer twice.

Instead use:

Idempotency Key

For example:

OrderId = 1001
PaymentAttempt = 1
IdempotencyKey = ORDER-1001-PAYMENT

The payment provider should treat repeated requests with the same key as the same logical operation where its API supports idempotency.


44. Handling Concurrent Orders

Suppose only one laptop remains.

Stock = 1

Two customers simultaneously order.

Customer A → Reserve
Customer B → Reserve

Inventory Service must use appropriate concurrency control.

For example:

UPDATE Inventory
SET AvailableQuantity = AvailableQuantity - 1
WHERE ProductId = @ProductId
AND AvailableQuantity >= 1;

Then check:

Rows affected = 1

Reservation succeeds.

If:

Rows affected = 0

reservation fails.

This prevents overselling.


45. Saga Failure Scenarios

Scenario 1

Order ✓
Inventory ✓
Payment ✓
Shipping ✓

Result:

Completed

Scenario 2

Order ✓
Inventory ✗

Compensation:

Cancel Order

Scenario 3

Order ✓
Inventory ✓
Payment ✗

Compensation:

Release Inventory
Cancel Order

Scenario 4

Order ✓
Inventory ✓
Payment ✓
Shipping ✗

Compensation:

Refund Payment
Release Inventory
Cancel Order

Scenario 5

Compensation fails

Solution:

Retry
 ↓
Retry
 ↓
Dead Letter Queue
 ↓
Operational Alert
 ↓
Manual Recovery

46. Azure Implementation

Since you're working with Azure/.NET, one possible architecture is:

ASP.NET Core
      |
      ↓
Order Service
      |
      ↓
Azure Service Bus
      |
      ↓
Saga Orchestrator
      |
      +----------------+
      |                |
      ↓                ↓
Inventory          Payment
Service            Service
      |                |
      ↓                ↓
 Azure SQL          Azure SQL

And:

Shipping Service
       |
       ↓
   Azure SQL

For monitoring:

Application Insights
Azure Monitor

For secrets:

Azure Key Vault

47. Azure Service Bus Structure

You might design:

Topic: ecommerce-events

Subscriptions:

order
inventory
payment
shipping
saga

Or use separate command queues:

inventory-commands
payment-commands
shipping-commands
order-commands

For orchestration, a command queue per service plus an event topic is often a clean conceptual model.


48. Message Flow

Example:

Order Service
     |
     | OrderCreated
     ↓
Service Bus
     |
     ↓
Saga Orchestrator
     |
     | ReserveInventory
     ↓
Inventory Queue
     |
     ↓
Inventory Service
     |
     | InventoryReserved
     ↓
Service Bus
     |
     ↓
Saga
     |
     | ProcessPayment
     ↓
Payment Queue

And so on.


49. Database Transactions Are Still Used

This is another important point.

Saga does not mean:

Don't use database transactions.

Each microservice should still use normal local transactions.

For example:

Inventory Service

BEGIN TRANSACTION

UPDATE Inventory

INSERT Reservation

INSERT OutboxMessage

COMMIT

This is a local ACID transaction.

Saga coordinates these local transactions.


50. The Golden Rule

Think about Saga like this:

Saga
 =
Multiple Local Transactions
 +
Messages
 +
State Machine
 +
Compensating Transactions
 +
Retry
 +
Idempotency
 +
Timeout Handling
 +
Observability

That is a much more accurate real-world definition than simply saying:

Saga is rollback for microservices.

It is not a distributed rollback mechanism.


51. Complete E-Commerce Flow

Here's the complete picture:

                         CUSTOMER
                            |
                            ↓
                       API Gateway
                            |
                            ↓
                     ORDER SERVICE
                            |
                       Create Order
                            |
                            ↓
                      OrderCreated
                            |
                            ↓
                   SAGA ORCHESTRATOR
                            |
                    Reserve Inventory
                            |
                            ↓
                   INVENTORY SERVICE
                            |
                   Inventory Reserved
                            |
                            ↓
                   SAGA ORCHESTRATOR
                            |
                     Process Payment
                            |
                            ↓
                    PAYMENT SERVICE
                            |
                     Payment Success
                            |
                            ↓
                   SAGA ORCHESTRATOR
                            |
                     Create Shipment
                            |
                            ↓
                   SHIPPING SERVICE
                            |
                    Shipment Created
                            |
                            ↓
                   SAGA ORCHESTRATOR
                            |
                            ↓
                     Confirm Order

Failure:

Payment Failed
      |
      ↓
Saga Orchestrator
      |
      +------→ Release Inventory
      |
      +------→ Cancel Order
      |
      ↓
Saga Compensated

52. Production-Grade Saga Checklist

When implementing Saga in a real application, consider all of these:

  • Saga ID

  • Correlation ID

  • Message ID

  • Idempotency

  • Saga state persistence

  • Local database transactions

  • Transactional Outbox

  • Reliable messaging

  • Retries

  • Exponential backoff

  • Timeouts

  • Dead-letter queues

  • Compensating transactions

  • Concurrency control

  • Optimistic/pessimistic locking where appropriate

  • Observability

  • Distributed tracing

  • Audit logging

  • Manual recovery

  • Poison message handling


53. Saga vs Two-Phase Commit

A common interview question is:

2PC

Coordinator
    |
    +--- DB1
    +--- DB2
    +--- DB3

Prepare
Prepare
Prepare

Commit
Commit
Commit

It attempts to provide distributed transactional atomicity, but can introduce blocking, coordination overhead, and operational complexity.

Saga:

Local Transaction
      ↓
Message
      ↓
Local Transaction
      ↓
Message

Failure:

Compensating Transaction

Saga is generally better suited to independently deployable microservices where business operations can be compensated.


54. Interview Answer

If an interviewer asks:

"Explain Saga Design Pattern with an e-commerce example."

A strong answer would be:

"Saga is a distributed transaction pattern used in microservices where a business transaction is divided into a sequence of local transactions. Each service commits its own transaction independently. If a later transaction fails, the Saga executes compensating transactions for the previously completed operations.

For example, in an e-commerce system, placing an order may involve creating the order, reserving inventory, processing payment, and creating a shipment. If payment fails after inventory has been reserved, the Saga doesn't perform a database rollback across services. Instead, it executes a compensating transaction to release the inventory and then cancels the order.

Saga can be implemented using choreography, where services communicate through events, or orchestration, where a central Saga orchestrator manages the workflow. In a production system, I would also use an outbox pattern, idempotent message processing, retries, timeouts, dead-letter queues, correlation IDs, and persistent Saga state to achieve reliable eventual consistency."


55. Recommended .NET Architecture

For a production .NET implementation, I would structure it approximately like this:

src
│
├── OrderService
│   ├── Controllers
│   ├── Domain
│   ├── Application
│   ├── Infrastructure
│   └── Messaging
│
├── InventoryService
│   ├── Domain
│   ├── Application
│   ├── Infrastructure
│   └── Messaging
│
├── PaymentService
│   ├── Domain
│   ├── Application
│   ├── Infrastructure
│   └── Messaging
│
├── ShippingService
│   ├── Domain
│   ├── Application
│   ├── Infrastructure
│   └── Messaging
│
└── OrderSaga
    ├── StateMachine
    ├── Commands
    ├── Events
    ├── Consumers
    └── Persistence

A particularly robust combination is:

Microservices
     +
Saga Orchestration
     +
Azure Service Bus
     +
Transactional Outbox
     +
Idempotent Consumers
     +
Retry/Timeout
     +
Dead Letter Queue
     +
Azure SQL
     +
Application Insights

The key idea to remember is:

Saga does not make multiple databases behave like one database. It coordinates independent local transactions and uses compensating actions to bring the overall business process to a consistent state.


Don't Copy

Protected by Copyscape Online Plagiarism Checker