Mastering Design Patterns in C# and ASP.NET Core
Part 4.5 – Mediator Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐⭐☆ Intermediate to Advanced
Prerequisites: C#, OOP, Interfaces, Dependency Injection, SOLID Principles, ASP.NET Core Web API, LINQ
Table of Contents
Introduction
What is the Mediator Design Pattern?
Why Do We Need the Mediator Pattern?
The Problem with Direct Object Communication
Mediator Pattern Solution
Mediator vs Direct Communication
Mediator Pattern Structure
UML Class Diagram
Components of the Mediator Pattern
Complete C# Console Application
ASP.NET Core Implementation
Dependency Injection with Mediator
Request/Response Communication
Command and Query Handlers
Mediator and CQRS
Banking Example
E-Commerce Example
Pipeline Behaviors
Validation
Logging
Authorization
Transaction Handling
MediatR-Style Architecture
Real-World Enterprise Scenarios
Advantages
Disadvantages
Best Practices
Common Mistakes
Mediator vs Observer
Mediator vs Facade
Mediator vs Command
Interview Questions
Key Takeaways
Conclusion
Coming Up Next
1. Introduction
In a small application, objects can communicate directly with each other.
For example:
OrderService
↓
PaymentService
↓
InventoryService
↓
NotificationService
At first, this seems straightforward.
But as the application grows, the number of relationships between objects can increase dramatically.
A large enterprise application might contain:
Order Service
Payment Service
Inventory Service
Customer Service
Notification Service
Shipping Service
Audit Service
Fraud Detection Service
Reporting Service
If every service communicates directly with every other service, the application can become difficult to maintain.
The Mediator Design Pattern provides a solution.
Instead of:
Object A → Object B
Object A → Object C
Object B → Object D
Object C → Object D
we can introduce a central communication component:
Mediator
/ | \
/ | \
↓ ↓ ↓
Object Object Object
The objects communicate through the mediator rather than directly with one another.
2. What is the Mediator Design Pattern?
Definition
The Mediator Design Pattern is a behavioral design pattern that defines an object responsible for encapsulating how a group of objects interact.
In simple terms:
Mediator centralizes communication between objects so that they do not need to know about each other directly.
Instead of this:
CustomerService → OrderService
OrderService → PaymentService
PaymentService → NotificationService
we can use:
CustomerService
↓
Mediator
↓
OrderService
PaymentService
NotificationService
This reduces direct dependencies between participating objects.
3. Why Do We Need the Mediator Pattern?
Imagine an e-commerce checkout process.
We have:
Order
Payment
Inventory
Shipping
Notification
Audit
Without a mediator, the checkout service could become tightly coupled:
CheckoutService
|
+---- PaymentService
|
+---- InventoryService
|
+---- ShippingService
|
+---- NotificationService
|
+---- AuditService
As more functionality is introduced, the service becomes increasingly complex.
For example:
CheckoutService
|
+-- PaymentService
+-- InventoryService
+-- ShippingService
+-- NotificationService
+-- AuditService
+-- FraudService
+-- LoyaltyService
This violates the spirit of several SOLID principles, particularly the Single Responsibility Principle and dependency-management goals.
The Mediator Pattern helps centralize the coordination.
4. The Problem with Direct Object Communication
Consider:
public class OrderService
{
private readonly PaymentService _paymentService;
private readonly InventoryService _inventoryService;
private readonly NotificationService _notificationService;
public OrderService(
PaymentService paymentService,
InventoryService inventoryService,
NotificationService notificationService)
{
_paymentService = paymentService;
_inventoryService = inventoryService;
_notificationService = notificationService;
}
}
This class directly knows about several other services.
Now imagine that each of those services also knows about several others.
We can eventually get:
A
/ | \
B C D
/ \ | / \
E F G H
This is sometimes called communication complexity or dependency explosion.
The more objects know about one another, the harder it becomes to:
Test
Modify
Reuse
Understand
Maintain
5. Mediator Pattern Solution
The Mediator Pattern introduces a central object.
+----------------+
| Mediator |
+-------+--------+
|
+-----------------+----------------+
| | |
↓ ↓ ↓
OrderService PaymentService InventoryService
Now the participating components don't need direct knowledge of each other.
The mediator coordinates communication.
6. Mediator vs Direct Communication
Without Mediator
OrderService
↓
PaymentService
OrderService
↓
InventoryService
OrderService
↓
NotificationService
The order service has multiple dependencies.
With Mediator
OrderService
↓
Mediator
/ | \
↓ ↓ ↓
Payment Inventory Notification
The communication becomes centralized.
7. Mediator Pattern Structure
The classic pattern consists of:
Client
↓
Mediator
↓
Colleagues
The participating objects are commonly called Colleagues.
For example:
Mediator
|
+-----------+-----------+
| | |
↓ ↓ ↓
Colleague Colleague Colleague
8. UML Class Diagram
A traditional UML representation looks like this:
+----------------------+
| <<interface>> |
| Mediator |
+----------------------+
| + Notify(...) |
+----------^-----------+
|
|
+----------------------+
| ConcreteMediator |
+----------------------+
| - colleagueA |
| - colleagueB |
+----------------------+
| + Notify(...) |
+----+------------+----+
| |
↓ ↓
+------------+ +------------+
| ColleagueA | | ColleagueB |
+------------+ +------------+
| - mediator | | - mediator |
+------------+ +------------+
The mediator knows the participating colleagues.
The colleagues know the mediator.
They don't need to know each other directly.
9. Components of the Mediator Pattern
1. Mediator
Defines communication between components.
Example:
public interface IMediator
{
void Notify(object sender, string eventName);
}
2. Concrete Mediator
Implements the coordination logic.
Example:
public class ConcreteMediator : IMediator
{
// Coordination logic
}
3. Colleagues
Objects that communicate through the mediator.
Examples:
OrderService
PaymentService
InventoryService
NotificationService
4. Client
Creates or uses the mediator and participating objects.
10. Complete C# Console Application
Let's create a simple banking example.
Suppose we have:
Account
Payment
Notification
Instead of allowing these classes to communicate directly, we'll use a mediator.
Step 1 – Mediator Interface
public interface IBankMediator
{
void Notify(object sender, string eventName);
}
11. Concrete Mediator
public class BankMediator : IBankMediator
{
public AccountService? AccountService { get; set; }
public NotificationService? NotificationService { get; set; }
public void Notify(object sender, string eventName)
{
if (eventName == "WithdrawalCompleted")
{
NotificationService?.SendNotification(
"Withdrawal completed successfully.");
}
if (eventName == "DepositCompleted")
{
NotificationService?.SendNotification(
"Deposit completed successfully.");
}
}
}
The mediator decides what should happen when an event occurs.
12. Account Service
public class AccountService
{
private readonly IBankMediator _mediator;
public AccountService(IBankMediator mediator)
{
_mediator = mediator;
}
public void Withdraw(decimal amount)
{
Console.WriteLine(
$"Withdrawal of {amount:C} completed.");
_mediator.Notify(
this,
"WithdrawalCompleted");
}
public void Deposit(decimal amount)
{
Console.WriteLine(
$"Deposit of {amount:C} completed.");
_mediator.Notify(
this,
"DepositCompleted");
}
}
Notice something important.
AccountService doesn't directly depend on:
NotificationService
It only knows:
IBankMediator
13. Notification Service
public class NotificationService
{
public void SendNotification(string message)
{
Console.WriteLine(
$"Notification: {message}");
}
}
14. Client Code
IBankMediator mediator = new BankMediator();
var bankMediator = (BankMediator)mediator;
var notificationService =
new NotificationService();
var accountService =
new AccountService(mediator);
bankMediator.AccountService = accountService;
bankMediator.NotificationService =
notificationService;
accountService.Deposit(1000);
accountService.Withdraw(250);
Output:
Deposit of $1,000.00 completed.
Notification: Deposit completed successfully.
Withdrawal of $250.00 completed.
Notification: Withdrawal completed successfully.
The communication flow is:
AccountService
↓
IBankMediator
↓
BankMediator
↓
NotificationService
15. Improving the Example
The previous example demonstrates the basic pattern.
However, production applications should avoid unnecessary casting and mutable mediator properties.
A cleaner approach is to inject dependencies into the concrete mediator.
For example:
public class BankMediator : IBankMediator
{
private readonly NotificationService _notificationService;
public BankMediator(
NotificationService notificationService)
{
_notificationService = notificationService;
}
public void Notify(
object sender,
string eventName)
{
switch (eventName)
{
case "WithdrawalCompleted":
_notificationService.SendNotification(
"Withdrawal completed successfully.");
break;
case "DepositCompleted":
_notificationService.SendNotification(
"Deposit completed successfully.");
break;
}
}
}
This version is easier to test and works better with dependency injection.
16. ASP.NET Core Implementation
Now let's implement a more practical version.
Suppose we have an order-processing API.
We want to coordinate:
Order
Payment
Inventory
Notification
17. Project Structure
A possible architecture:
MyShop
│
├── Controllers
│ └── OrdersController.cs
│
├── Mediator
│ ├── IOrderMediator.cs
│ └── OrderMediator.cs
│
├── Services
│ ├── OrderService.cs
│ ├── PaymentService.cs
│ ├── InventoryService.cs
│ └── NotificationService.cs
│
├── Models
│ └── Order.cs
│
└── Program.cs
18. Order Model
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public decimal Amount { get; set; }
}
19. Mediator Interface
public interface IOrderMediator
{
Task<bool> PlaceOrderAsync(Order order);
}
20. Payment Service
public interface IPaymentService
{
Task<bool> ProcessPaymentAsync(
decimal amount);
}
Implementation:
public class PaymentService : IPaymentService
{
public Task<bool> ProcessPaymentAsync(
decimal amount)
{
Console.WriteLine(
$"Payment processed: {amount:C}");
return Task.FromResult(true);
}
}
21. Inventory Service
public interface IInventoryService
{
Task<bool> ReserveInventoryAsync(
int orderId);
}
Implementation:
public class InventoryService : IInventoryService
{
public Task<bool> ReserveInventoryAsync(
int orderId)
{
Console.WriteLine(
$"Inventory reserved for order {orderId}");
return Task.FromResult(true);
}
}
22. Notification Service
public interface INotificationService
{
Task SendOrderConfirmationAsync(
int orderId);
}
Implementation:
public class NotificationService :
INotificationService
{
public Task SendOrderConfirmationAsync(
int orderId)
{
Console.WriteLine(
$"Confirmation sent for order {orderId}");
return Task.CompletedTask;
}
}
23. Concrete Mediator
public class OrderMediator : IOrderMediator
{
private readonly IPaymentService _paymentService;
private readonly IInventoryService _inventoryService;
private readonly INotificationService _notificationService;
public OrderMediator(
IPaymentService paymentService,
IInventoryService inventoryService,
INotificationService notificationService)
{
_paymentService = paymentService;
_inventoryService = inventoryService;
_notificationService = notificationService;
}
public async Task<bool> PlaceOrderAsync(
Order order)
{
var paymentSuccessful =
await _paymentService
.ProcessPaymentAsync(order.Amount);
if (!paymentSuccessful)
{
return false;
}
var inventoryReserved =
await _inventoryService
.ReserveInventoryAsync(order.Id);
if (!inventoryReserved)
{
return false;
}
await _notificationService
.SendOrderConfirmationAsync(order.Id);
return true;
}
}
The mediator coordinates the workflow.
24. Orders Controller
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IOrderMediator _mediator;
public OrdersController(
IOrderMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> PlaceOrder(
Order order)
{
var result =
await _mediator.PlaceOrderAsync(order);
if (!result)
{
return BadRequest(
"Unable to place order.");
}
return Ok(
new
{
Message = "Order placed successfully."
});
}
}
The controller now has a very simple responsibility.
HTTP Request
↓
Controller
↓
Mediator
↓
Payment
Inventory
Notification
25. Register Services with Dependency Injection
In Program.cs:
builder.Services.AddScoped<IOrderMediator,
OrderMediator>();
builder.Services.AddScoped<IPaymentService,
PaymentService>();
builder.Services.AddScoped<IInventoryService,
InventoryService>();
builder.Services.AddScoped<INotificationService,
NotificationService>();
This allows ASP.NET Core's built-in dependency injection container to construct the object graph.
26. Request/Response Communication
A common modern implementation of the Mediator Pattern uses a request and handler.
For example:
PlaceOrderRequest
↓
PlaceOrderHandler
↓
Order Processing
↓
PlaceOrderResponse
Conceptually:
Controller
↓
Send(Request)
↓
Mediator
↓
Handler
↓
Service / Repository
↓
Response
This is extremely common in CQRS-style architectures.
27. Command and Handler Architecture
Consider:
public record CreateOrderCommand(
int CustomerId,
decimal Amount);
A handler processes the command:
public class CreateOrderHandler
{
public async Task<int> Handle(
CreateOrderCommand command)
{
// Create order
// Save to database
// Return ID
return 1001;
}
}
The controller does not need to know how the command is processed.
It simply sends the request to the appropriate handler.
28. Mediator and CQRS
Mediator and CQRS are different concepts, but they work extremely well together.
CQRS
CQRS stands for:
Command Query Responsibility Segregation
It separates:
Commands
from:
Queries
Commands change state.
Queries retrieve data.
For example:
CreateOrderCommand
UpdateCustomerCommand
CancelOrderCommand
and:
GetOrderByIdQuery
GetCustomerQuery
GetOrderHistoryQuery
Mediator can route each request to the appropriate handler.
29. CQRS + Mediator Architecture
A typical architecture:
Controller
|
↓
Mediator
|
+----------+----------+
| |
↓ ↓
Command Query
| |
↓ ↓
CommandHandler QueryHandler
| |
↓ ↓
Write Database Read Database
This provides a clean separation between application requests.
30. Example: Command
public record CreateOrderCommand(
int CustomerId,
decimal Amount);
Handler:
public class CreateOrderHandler
{
public async Task<int> Handle(
CreateOrderCommand command)
{
Console.WriteLine(
$"Creating order for customer {command.CustomerId}");
// Save order
return 1001;
}
}
31. Example: Query
public record GetOrderQuery(int OrderId);
Handler:
public class GetOrderHandler
{
public async Task<Order?> Handle(
GetOrderQuery query)
{
// Read order from database
return new Order
{
Id = query.OrderId
};
}
}
The application now separates:
Commands → Change State
Queries → Read State
32. Pipeline Behaviors
One of the most powerful ideas in mediator-based architecture is the pipeline.
A request can flow through multiple behaviors:
Request
↓
Logging
↓
Validation
↓
Authorization
↓
Transaction
↓
Handler
↓
Response
This is similar to ASP.NET Core middleware.
33. Logging Pipeline
Instead of adding logging manually to every handler:
Console.WriteLine("Starting handler");
we can centralize logging.
Conceptually:
Request
↓
Logging Behavior
↓
Handler
This provides consistent logging.
34. Validation Pipeline
A validation behavior can inspect requests before the handler runs.
CreateOrderCommand
↓
Validation
↓
Valid?
/ \
No Yes
↓ ↓
Error Handler
For example:
Amount > 0
CustomerId > 0
Order items exist
If validation fails, the handler doesn't need to execute.
35. Authorization Pipeline
Authorization can also be performed before the handler.
Request
↓
Authentication
↓
Authorization
↓
Handler
For example:
User
↓
Has Permission?
↓
Yes → Handler
No → Access Denied
This keeps authorization-related concerns out of business handlers where practical.
36. Transaction Pipeline
For commands that modify multiple records:
Request
↓
Begin Transaction
↓
Handler
↓
Save Changes
↓
Commit
If something fails:
Exception
↓
Rollback
This can be implemented as a pipeline behavior where the transaction boundary is appropriate.
37. MediatR-Style Architecture
A popular .NET approach has historically been the MediatR library, which follows mediator-style request/handler concepts.
A typical structure looks like:
Application
│
├── Commands
│ └── CreateOrder
│ ├── CreateOrderCommand
│ └── CreateOrderHandler
│
├── Queries
│ └── GetOrder
│ ├── GetOrderQuery
│ └── GetOrderHandler
│
└── Behaviors
├── LoggingBehavior
├── ValidationBehavior
└── TransactionBehavior
The important architectural lesson is not the library itself.
The important concept is:
One request can be routed to one focused handler, while cross-cutting concerns can be handled in a pipeline.
38. Banking Example
Consider a banking application.
We may have:
TransferMoneyCommand
DepositMoneyCommand
WithdrawMoneyCommand
GetAccountBalanceQuery
GetTransactionHistoryQuery
A mediator can route each request.
Mediator
|
+-------------+-------------+
| | |
↓ ↓ ↓
TransferHandler DepositHandler WithdrawHandler
|
↓
Account / Transaction Services
For a money transfer:
Transfer Request
↓
Validation
↓
Authorization
↓
Transaction
↓
Debit Account
↓
Credit Account
↓
Audit
↓
Commit
The controller remains simple.
39. E-Commerce Example
Suppose a customer places an order.
CreateOrderCommand
↓
Mediator
↓
CreateOrderHandler
|
+── Validate Customer
|
+── Check Inventory
|
+── Calculate Total
|
+── Process Payment
|
+── Create Order
|
+── Publish Event
Each responsibility can be delegated to appropriate application/domain services.
The handler coordinates the use case rather than becoming a giant business object itself.
40. Real-World Enterprise Scenarios
Mediator-style architecture can be useful for:
Banking
Fund transfers
Account operations
Loan applications
Payment processing
E-Commerce
Create order
Cancel order
Process payment
Update inventory
Healthcare
Patient registration
Appointment scheduling
Claims processing
Insurance
Claim submission
Policy updates
Premium calculation
Logistics
Shipment creation
Delivery scheduling
Tracking updates
HR
Employee onboarding
Leave requests
Payroll operations
41. Advantages of Mediator Pattern
1. Reduces Coupling
Objects don't need direct references to one another.
2. Centralizes Communication
Communication rules can be located in the mediator or request-handling pipeline.
3. Improves Testability
Individual handlers/services can be tested independently.
4. Supports Single Responsibility
Handlers can focus on one request/use case.
5. Works Well with CQRS
Mediator-style request routing is a natural fit for:
Commands
Queries
Handlers
6. Centralizes Cross-Cutting Concerns
Pipeline behaviors can handle:
Logging
Validation
Authorization
Transactions
Performance Monitoring
7. Cleaner Controllers
Controllers can become thin:
HTTP
↓
Request
↓
Mediator
↓
Handler
42. Disadvantages of Mediator Pattern
1. Mediator Can Become Too Large
If all business logic is placed into one mediator class, it becomes a God Object.
Bad design:
Mediator
├── Orders
├── Payments
├── Inventory
├── Customers
├── Reports
├── Shipping
└── Everything Else
The mediator should coordinate, not contain the entire business domain.
2. Additional Abstraction
For a small application, mediator infrastructure can be unnecessary.
3. Debugging Can Be Less Direct
Instead of:
Controller → Service
the flow can become:
Controller
→ Mediator
→ Pipeline
→ Handler
→ Service
→ Repository
This requires developers to understand the architecture.
4. Overuse Can Create Excessive Classes
A very small operation may not justify:
Request
Handler
Validator
Behavior
Response
Use the architecture where it adds value.
43. Best Practices
1. Keep Handlers Focused
A handler should represent a specific use case.
For example:
CreateOrderHandler
CancelOrderHandler
GetOrderHandler
2. Keep Business Logic in Appropriate Layers
Don't turn the mediator into a business-logic container.
Use:
Handler
↓
Domain/Application Services
↓
Repository
where appropriate.
3. Use Dependency Injection
ASP.NET Core DI works naturally with mediator-based architectures.
4. Keep Controllers Thin
Controllers should primarily handle:
HTTP concerns
Model binding
Authentication context
Response mapping
Business workflows should not become controller code.
5. Use Pipeline Behaviors for Cross-Cutting Concerns
Good candidates include:
Logging
Validation
Authorization
Transactions
Performance measurement
6. Don't Introduce a Mediator Everywhere
For simple applications:
Controller → Service
may be perfectly appropriate.
Architecture should solve a real problem rather than introduce complexity for its own sake.
7. Keep Commands and Queries Clear
Use meaningful names:
CreateOrderCommand
CancelOrderCommand
GetOrderQuery
GetCustomerQuery
44. Common Mistakes
Mistake 1 – Putting Everything in the Mediator
The mediator should coordinate communication.
It should not become the entire application's business layer.
Mistake 2 – Creating One Giant Handler
For example:
OrderHandler
containing:
Create
Update
Cancel
Refund
Ship
Track
Return
Prefer focused handlers.
Mistake 3 – Confusing Mediator with CQRS
Mediator and CQRS are different.
Mediator
→ Communication / request routing
CQRS
→ Separation of commands and queries
They can be used together, but one does not mean the other.
Mistake 4 – Overusing Pipeline Behaviors
Not every operation needs ten layers of pipeline processing.
Use behaviors for genuine cross-cutting concerns.
Mistake 5 – Hiding Important Business Flow
If the architecture has too many abstractions, developers may struggle to understand the actual workflow.
Keep the request flow discoverable.
45. Mediator vs Observer
These patterns are often confused.
| Mediator | Observer |
|---|---|
| Centralizes communication | Broadcasts notifications |
| Usually coordinates interactions | One-to-many relationship |
| Participants communicate through mediator | Observers subscribe to subject |
| Focuses on coordination | Focuses on event notification |
Mediator
A → Mediator → B
Observer
Subject
/ | \
↓ ↓ ↓
Observer Observer Observer
46. Mediator vs Facade
Both can appear to centralize access, but they have different goals.
| Mediator | Facade |
|---|---|
| Coordinates communication between objects | Simplifies access to a subsystem |
| Objects communicate through mediator | Client communicates with facade |
| Focuses on collaboration | Focuses on simplification |
| Behavioral pattern | Structural pattern |
Facade:
Client
↓
Facade
↓
Subsystem
Mediator:
Object A
↓
Mediator
↓
Object B
47. Mediator vs Command
These patterns also work together.
Command
Encapsulates a request as an object.
CreateOrderCommand
Mediator
Routes the request to the appropriate handler.
Command
↓
Mediator
↓
Handler
Therefore, they are complementary rather than competing patterns.
48. Mediator vs Chain of Responsibility
The distinction is important.
Chain of Responsibility
A request passes through a chain:
Request
↓
Handler A
↓
Handler B
↓
Handler C
Mediator
Objects communicate through a central mediator:
Object A
↓
Mediator
↓
Object B
A mediator-based application can also contain pipeline behaviors that resemble a chain.
49. Interview Questions
Beginner
1. What is the Mediator Design Pattern?
It is a behavioral pattern that centralizes communication between objects so they don't need direct references to one another.
2. What problem does Mediator solve?
It reduces tightly coupled communication between multiple objects.
3. What type of design pattern is Mediator?
It is a Behavioral Design Pattern.
4. What are Colleagues?
They are the objects that communicate through the mediator.
5. What is the role of the Mediator?
It coordinates communication and interaction between participating objects.
Intermediate
6. What is the difference between Mediator and Facade?
Facade simplifies access to a subsystem.
Mediator coordinates communication between participating objects.
7. What is the difference between Mediator and Observer?
Mediator centralizes and coordinates communication.
Observer provides one-to-many notification.
8. Is Mediator related to CQRS?
They are separate concepts, but they are frequently used together.
Mediator can route commands and queries to their respective handlers.
9. What is a Handler?
A handler is responsible for processing a specific request, command, or query.
10. What is a pipeline behavior?
A pipeline behavior executes around request handling and is useful for cross-cutting concerns such as:
Logging
Validation
Authorization
Transactions
Performance monitoring
50. Advanced Interview Questions
11. Why should a mediator not contain all business logic?
Because it can become a God Object with too many responsibilities.
Business logic should remain in appropriate domain/application services and handlers.
12. How does Mediator improve testability?
Components can depend on abstractions and focused handlers can be tested independently.
13. Can Mediator be used without CQRS?
Yes.
Mediator is a communication pattern. CQRS is an architectural pattern for separating commands and queries.
14. Can CQRS be used without Mediator?
Yes.
CQRS doesn't require a mediator implementation.
15. How does Mediator work with dependency injection?
The mediator, handlers, services, repositories, and behaviors can be registered with ASP.NET Core's dependency injection container.
16. Why are pipeline behaviors useful?
They allow cross-cutting concerns to be applied consistently without duplicating code in every handler.
17. What is a common disadvantage of Mediator?
It can add unnecessary abstraction and make simple flows more complicated.
18. When should you avoid Mediator?
For small applications where direct service calls are clear and manageable.
19. How does Mediator relate to Clean Architecture?
Mediator-style request/handler architecture can fit naturally into the application layer of Clean Architecture.
For example:
API
↓
Application
↓
Domain
↓
Infrastructure
Requests and handlers can live in the Application layer while infrastructure concerns remain separated.
20. What is the difference between a Mediator and a Service?
A service generally provides a business capability.
A mediator primarily coordinates communication or routes requests between components.
51. Practical Enterprise Architecture
A modern ASP.NET Core application may use:
Client
|
↓
ASP.NET Core API
|
↓
Controller
|
↓
Mediator
|
+--------+--------+
| |
↓ ↓
Command Query
| |
↓ ↓
Handler Handler
| |
↓ ↓
Domain Services Read Services
| |
↓ ↓
Repository / DB Repository / DB
Cross-cutting concerns can surround the handler:
Request
↓
Logging
↓
Validation
↓
Authorization
↓
Transaction
↓
Handler
↓
Response
This architecture can provide a clean and maintainable application layer when used appropriately.
52. Complete Request Flow Example
Let's consider:
POST /api/orders
The request might flow through the application as:
HTTP Request
↓
OrdersController
↓
CreateOrderCommand
↓
Mediator
↓
Validation Behavior
↓
Authorization Behavior
↓
Transaction Behavior
↓
CreateOrderHandler
↓
OrderService
↓
Repository
↓
Database
↓
Response
This gives developers a predictable request-processing pipeline.
53. Key Takeaways
Remember these points:
1. Mediator is a Behavioral Design Pattern
It focuses on communication and collaboration between objects.
2. It reduces direct coupling
Instead of:
A → B
A → C
B → C
we use:
A → Mediator
B → Mediator
C → Mediator
3. Colleagues communicate through the mediator
They don't need direct knowledge of one another.
4. Mediator works well with ASP.NET Core
It can help structure application-level request handling.
5. Mediator and CQRS are different
Mediator handles communication/routing.
CQRS separates commands and queries.
6. Commands and Queries can have dedicated handlers
Command
↓
CommandHandler
Query
↓
QueryHandler
7. Pipeline behaviors handle cross-cutting concerns
Examples:
Logging
Validation
Authorization
Transactions
Performance
8. Don't overuse Mediator
For simple applications, a direct:
Controller → Service
architecture can be better.
Conclusion
The Mediator Design Pattern is an important behavioral pattern for designing loosely coupled applications.
Without Mediator, objects can become tightly connected:
A → B
A → C
B → D
C → D
As the application grows, this communication structure can become difficult to understand and maintain.
Mediator introduces a central communication point:
Mediator
/ | \
↓ ↓ ↓
A B C
In modern ASP.NET Core applications, mediator-style architecture is especially useful when combined with:
Dependency Injection
CQRS
Command/Query handlers
Validation pipelines
Logging pipelines
Authorization
Transaction management
Clean Architecture
A typical enterprise request flow can become:
Controller
↓
Mediator
↓
Pipeline Behaviors
↓
Handler
↓
Application/Domain Services
↓
Repository
↓
Database
However, the Mediator Pattern should not be treated as a mandatory architecture for every application.
For a small application, direct service calls may be simpler.
For a large enterprise application with many use cases and cross-cutting requirements, mediator-style request/handler architecture can significantly improve organization and separation of responsibilities.
The key idea is simple: Mediator reduces direct communication between objects by introducing a central coordinator, making complex interactions easier to organize, test, and maintain.
🚀 Coming Up Next: Part 4.6 – Memento Design Pattern
In the next article, we'll explore the Memento Design Pattern, including:
What is the Memento Pattern?
Why do we need it?
Originator, Memento, and Caretaker
Encapsulation and state preservation
UML Class Diagram
Complete C# Console Application
Undo/Redo implementation
ASP.NET Core implementation
Banking transaction rollback scenario
Document version history
Configuration snapshots
Database transaction concepts
State restoration
Real-world enterprise examples
Advantages and disadvantages
Best practices
Common mistakes
Memento vs Command
Memento vs Snapshot
Memento vs Prototype
Interview questions
The Memento Pattern will be particularly useful for understanding how applications can capture and restore an object's previous state while keeping the internal details of that state encapsulated.
