Wednesday, July 29, 2026

Memento Design Pattern

Mastering Design Patterns in C# and ASP.NET Core

Part 4.6 – Memento Design Pattern

Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐☆☆ Intermediate
Prerequisites: C#, OOP, Classes, Interfaces, Encapsulation, Dependency Injection, ASP.NET Core


Table of Contents

  1. Introduction

  2. What is the Memento Design Pattern?

  3. Why Do We Need the Memento Pattern?

  4. The Problem with Restoring Object State

  5. Memento Pattern Solution

  6. Originator, Memento, and Caretaker

  7. UML Class Diagram

  8. How the Memento Pattern Works

  9. Complete C# Console Application

  10. Undo/Redo Example

  11. Immutable Memento

  12. ASP.NET Core Implementation

  13. Banking Transaction Example

  14. Document Version History Example

  15. Configuration Snapshot Example

  16. Database Transaction and Memento

  17. Memento vs Command

  18. Memento vs Prototype

  19. Memento vs Snapshot

  20. Advantages

  21. Disadvantages

  22. Best Practices

  23. Common Mistakes

  24. Real-World Enterprise Scenarios

  25. Interview Questions

  26. Key Takeaways

  27. Conclusion

  28. Coming Up Next


1. Introduction

Applications frequently need the ability to save an object's state and restore it later.

For example:

  • A text editor needs Undo.

  • A drawing application needs previous versions.

  • A configuration system may need rollback.

  • A workflow engine may need to restore a previous state.

  • A game needs save points.

  • A banking application may need to preserve transaction state.

  • An application may need to maintain document versions.

Consider a document editor:

Document State
     ↓
Version 1
     ↓
Version 2
     ↓
Version 3
     ↓
Undo
     ↓
Restore Version 2

How can we save the previous state without exposing the internal implementation of the object?

This is where the Memento Design Pattern becomes useful.


2. What is the Memento Design Pattern?

The Memento Design Pattern is a behavioral design pattern that allows an object to capture and restore its previous state without exposing its internal implementation details.

The key idea is:

Capture an object's state at a particular point in time so that it can be restored later.

A simplified representation is:

Object
  ↓
Create Snapshot
  ↓
Memento
  ↓
Store
  ↓
Restore Later

The object whose state is saved is called the Originator.

The saved state is represented by a Memento.

The object that stores the mementos is called the Caretaker.


3. Why Do We Need the Memento Pattern?

Suppose we have:

public class Document
{
    public string Title { get; set; }

    public string Content { get; set; }

    public string Font { get; set; }
}

Now imagine that we want to implement:

Edit
Edit
Edit
Undo
Undo
Redo

One approach might be to expose all properties publicly and allow another class to copy them.

But this can lead to problems.

The caretaker may become tightly coupled to the internal structure of Document.

For example:

var title = document.Title;
var content = document.Content;
var font = document.Font;

Later, suppose we add:

public string BackgroundColor { get; set; }

Now every state-saving component may need to change.

This violates encapsulation.

The Memento Pattern provides a better abstraction.


4. The Problem with Restoring Object State

Consider a complex object:

Order
 ├── Customer
 ├── Items
 ├── Payment
 ├── Shipping
 ├── Discount
 └── Status

Saving and restoring the complete state manually can become complicated.

We don't want external classes to understand every internal detail.

Ideally:

Caretaker
   |
   | "Save"
   ↓
Originator
   |
   ↓
Memento

The caretaker stores the snapshot but does not need to understand its internal details.


5. Memento Pattern Solution

The Memento Pattern introduces three major components:

Originator
    |
    | creates
    ↓
 Memento
    ↑
    |
Caretaker

Originator

The object whose state needs to be saved.

Memento

Stores the state of the Originator.

Caretaker

Stores and manages Mementos.


6. Originator, Memento, and Caretaker

6.1 Originator

The Originator:

  • Maintains current state.

  • Creates a Memento.

  • Restores state from a Memento.

Example:

public class Document
{
    public string Content { get; private set; }

    public Memento Save()
    {
        return new Memento(Content);
    }

    public void Restore(Memento memento)
    {
        Content = memento.Content;
    }
}

6.2 Memento

The Memento stores a snapshot.

public class Memento
{
    public string Content { get; }

    public Memento(string content)
    {
        Content = content;
    }
}

6.3 Caretaker

The Caretaker stores snapshots.

public class History
{
    private readonly Stack<Memento> _history = new();

    public void Save(Memento memento)
    {
        _history.Push(memento);
    }

    public Memento Undo()
    {
        return _history.Pop();
    }
}

The important point is that the Caretaker doesn't modify the document's internal state directly.


7. UML Class Diagram

The classic UML structure looks like this:

                 +------------------+
                 |    Originator    |
                 +------------------+
                 | - state          |
                 +------------------+
                 | + Save()         |
                 | + Restore()      |
                 +--------+---------+
                          |
                          | creates
                          ↓
                 +------------------+
                 |     Memento      |
                 +------------------+
                 | - state          |
                 +------------------+
                 | + State          |
                 +------------------+
                          ↑
                          |
                          | stores
                 +--------+---------+
                 |    Caretaker     |
                 +------------------+
                 | - history        |
                 +------------------+
                 | + Save()         |
                 | + Undo()         |
                 +------------------+

8. How the Memento Pattern Works

Suppose a document has the following states.

State 1
Content = "Hello"

Save it:

Document
   ↓
Memento 1

Change it:

State 2
Content = "Hello World"

Save again:

Document
   ↓
Memento 2

Change again:

State 3
Content = "Hello World!"

Now the history contains:

History
 ├── Memento 2
 └── Memento 1

If the user chooses Undo:

Current State
     ↓
Restore Memento 2
     ↓
"Hello World"

9. Complete C# Console Application

Let's create a simple document editor.

Step 1 – Memento

public class DocumentMemento
{
    public string Content { get; }

    public DocumentMemento(string content)
    {
        Content = content;
    }
}

Step 2 – Originator

public class Document
{
    public string Content { get; private set; } = string.Empty;

    public void Write(string text)
    {
        Content += text;
    }

    public DocumentMemento Save()
    {
        return new DocumentMemento(Content);
    }

    public void Restore(DocumentMemento memento)
    {
        Content = memento.Content;
    }

    public void Show()
    {
        Console.WriteLine($"Document: {Content}");
    }
}

Step 3 – Caretaker

public class DocumentHistory
{
    private readonly Stack<DocumentMemento> _history = new();

    public void Save(DocumentMemento memento)
    {
        _history.Push(memento);
    }

    public DocumentMemento? Undo()
    {
        if (_history.Count == 0)
        {
            return null;
        }

        return _history.Pop();
    }
}

Step 4 – Program

var document = new Document();
var history = new DocumentHistory();

document.Write("Hello");
history.Save(document.Save());

document.Write(" World");
history.Save(document.Save());

document.Write("!");
document.Show();

var previousState = history.Undo();

if (previousState != null)
{
    document.Restore(previousState);
}

document.Show();

previousState = history.Undo();

if (previousState != null)
{
    document.Restore(previousState);
}

document.Show();

Output:

Document: Hello World!
Document: Hello World
Document: Hello

The flow is:

Write "Hello"
    ↓
Save
    ↓
Write " World"
    ↓
Save
    ↓
Write "!"
    ↓
Undo
    ↓
"Hello World"
    ↓
Undo
    ↓
"Hello"

10. Implementing Undo/Redo

A single stack is sufficient for Undo.

For both Undo and Redo, we typically need two stacks:

Undo Stack
Redo Stack

Conceptually:

             Current State
                  |
        +---------+---------+
        |                   |
    Undo Stack          Redo Stack

When the user changes the document:

Current
   ↓
Undo Stack

When Undo is performed:

Undo Stack
   ↓
Current
   ↓
Redo Stack

When Redo is performed:

Redo Stack
   ↓
Current
   ↓
Undo Stack

Complete Undo/Redo Example

public class DocumentHistory
{
    private readonly Stack<DocumentMemento> _undo = new();
    private readonly Stack<DocumentMemento> _redo = new();

    public void Save(DocumentMemento memento)
    {
        _undo.Push(memento);
        _redo.Clear();
    }

    public DocumentMemento? Undo(
        DocumentMemento current)
    {
        if (_undo.Count == 0)
        {
            return null;
        }

        _redo.Push(current);

        return _undo.Pop();
    }

    public DocumentMemento? Redo(
        DocumentMemento current)
    {
        if (_redo.Count == 0)
        {
            return null;
        }

        _undo.Push(current);

        return _redo.Pop();
    }
}

The important concept is:

New Change
   ↓
Clear Redo History

This is standard undo/redo behavior.


11. Immutable Memento

A Memento should ideally be immutable.

For example:

public sealed class DocumentMemento
{
    public string Content { get; }

    public DocumentMemento(string content)
    {
        Content = content;
    }
}

External code cannot change:

memento.Content

after construction.

This makes the snapshot safer and more predictable.


12. Using C# Records for Memento

Modern C# provides a convenient option using record.

public record DocumentMemento(
    string Content);

The Originator can create it:

public DocumentMemento Save()
{
    return new DocumentMemento(Content);
}

This is concise and works well when the snapshot is primarily data.


13. ASP.NET Core Implementation

Let's build a simplified document versioning API.

Suppose the API provides:

POST /api/documents
PUT  /api/documents/{id}
POST /api/documents/{id}/undo
GET  /api/documents/{id}

We can use a Memento-like snapshot model.


14. Document Model

public class Document
{
    public int Id { get; set; }

    public string Title { get; set; } = string.Empty;

    public string Content { get; set; } = string.Empty;
}

15. Document Snapshot

public record DocumentSnapshot(
    int Id,
    string Title,
    string Content);

This snapshot represents a point-in-time state.


16. Document Service

public interface IDocumentService
{
    Document Get(int id);

    void Update(
        int id,
        string title,
        string content);

    void Undo(int id);
}

Implementation:

public class DocumentService : IDocumentService
{
    private readonly Dictionary<int, Document> _documents = new();

    private readonly Dictionary<
        int,
        Stack<DocumentSnapshot>> _history = new();

    public Document Get(int id)
    {
        return _documents[id];
    }

    public void Update(
        int id,
        string title,
        string content)
    {
        var document = _documents[id];

        if (!_history.ContainsKey(id))
        {
            _history[id] =
                new Stack<DocumentSnapshot>();
        }

        _history[id].Push(
            new DocumentSnapshot(
                document.Id,
                document.Title,
                document.Content));

        document.Title = title;
        document.Content = content;
    }

    public void Undo(int id)
    {
        if (!_history.ContainsKey(id) ||
            _history[id].Count == 0)
        {
            return;
        }

        var snapshot = _history[id].Pop();

        var document = _documents[id];

        document.Title = snapshot.Title;
        document.Content = snapshot.Content;
    }
}

This is a simplified in-memory example.

In a production system, snapshots would generally need appropriate persistence and concurrency handling.


17. ASP.NET Core Controller

[ApiController]
[Route("api/documents")]
public class DocumentsController : ControllerBase
{
    private readonly IDocumentService _service;

    public DocumentsController(
        IDocumentService service)
    {
        _service = service;
    }

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var document = _service.Get(id);

        return Ok(document);
    }

    [HttpPost("{id}/undo")]
    public IActionResult Undo(int id)
    {
        _service.Undo(id);

        return Ok(new
        {
            Message = "Document restored successfully."
        });
    }
}

The controller doesn't need to know how snapshots are managed.


18. Register the Service

In Program.cs:

builder.Services.AddSingleton<IDocumentService,
                              DocumentService>();

For a real database-backed implementation, the lifetime and persistence strategy should be designed according to the application's requirements rather than blindly using Singleton.


19. Banking Transaction Example

The Memento Pattern can be useful when an application needs to capture a business object's state before performing a change.

Consider:

Bank Account
     |
     +-- Balance
     +-- Status
     +-- Account Type

Before a complex operation:

Account State
     ↓
Create Snapshot
     ↓
Perform Operation

If the operation needs to be reversed at the application level:

Snapshot
    ↓
Restore

For example:

public record AccountMemento(
    decimal Balance,
    string Status);

Originator:

public class BankAccount
{
    public decimal Balance { get; private set; }

    public string Status { get; private set; }
        = "Active";

    public AccountMemento Save()
    {
        return new AccountMemento(
            Balance,
            Status);
    }

    public void Restore(
        AccountMemento memento)
    {
        Balance = memento.Balance;
        Status = memento.Status;
    }

    public void Deposit(decimal amount)
    {
        Balance += amount;
    }
}

The pattern can preserve application-level state.

However, it is important to distinguish this from a database transaction.


20. Memento vs Database Transaction

These concepts are not interchangeable.

Memento

Used to preserve and restore object/application state.

Object
 ↓
Snapshot
 ↓
Restore

Database Transaction

Used to ensure atomic database operations.

BEGIN TRANSACTION
     ↓
UPDATE
     ↓
INSERT
     ↓
COMMIT

If something fails:

ROLLBACK

Therefore:

Use database transactions for database atomicity and Memento when you need application-level state snapshots.


21. Document Version History

One of the best real-world examples is document versioning.

Suppose:

Document
Version 1
Version 2
Version 3
Version 4

The application can store snapshots:

Version 1 → Snapshot
Version 2 → Snapshot
Version 3 → Snapshot
Version 4 → Current

The user can then restore an earlier version.

This concept appears in many applications involving:

  • Documents

  • Configuration

  • Templates

  • Content management

  • Workflow definitions


22. Configuration Snapshot Example

Imagine an application configuration:

public class ApplicationSettings
{
    public int MaxRetries { get; set; }

    public int TimeoutSeconds { get; set; }

    public bool EnableFeatureX { get; set; }
}

Before changing configuration:

Current Configuration
        ↓
Create Snapshot
        ↓
Apply New Configuration

If the new configuration causes problems:

Snapshot
    ↓
Restore

This is useful in administrative systems where users can experiment with configuration changes.


23. Memento and Serialization

Sometimes snapshots are persisted as:

JSON
XML
Binary
Database Records

For example:

public record ConfigurationMemento(
    int MaxRetries,
    int TimeoutSeconds,
    bool EnableFeatureX);

The snapshot could be serialized into a persistence store.

However, if the state is large, storing complete snapshots for every change can become expensive.

That leads to an important design consideration:

Memento trades memory/storage for simpler state restoration.


24. Large Object State and Memory Considerations

Suppose an application creates:

1,000 snapshots

and each snapshot is:

5 MB

The storage requirement could become significant.

Therefore, Memento should be carefully designed for large objects.

Possible alternatives include:

  • Store only changed properties.

  • Store version identifiers.

  • Use database versioning.

  • Use event sourcing.

  • Store compressed snapshots.

  • Limit history size.


25. Memento vs Command

These patterns are frequently used together.

Command

Represents an operation:

DeleteDocumentCommand
UpdateOrderCommand
ChangeAddressCommand

Memento

Stores state:

Before State
After State

They can work together for Undo.

Command
   ↓
Execute
   ↓
Memento
   ↓
Undo

For example:

UpdateDocumentCommand
        ↓
Save Memento
        ↓
Update Document
        ↓
Execute

Undo:

Memento
   ↓
Restore

26. Memento vs Prototype

Both involve copying state, but they have different purposes.

MementoPrototype
Saves state for later restorationCreates new objects by cloning
Primarily supports undo/rollbackPrimarily supports object creation
Behavioral patternCreational pattern
Focuses on state historyFocuses on object creation

Memento:

Object
 ↓
Snapshot
 ↓
Restore

Prototype:

Existing Object
 ↓
Clone
 ↓
New Object

27. Memento vs Snapshot

The word "snapshot" is often used informally for the state stored by a Memento.

However, the design pattern provides a broader structure:

Originator
    ↓
Memento
    ↓
Caretaker

A snapshot can simply mean a saved representation of state.

Memento is specifically about encapsulating an object's state so it can be restored later without exposing its internals.


28. Advantages of Memento Pattern

1. Preserves Encapsulation

The Caretaker doesn't need to know the internal implementation of the Originator.


2. Supports Undo

One of the most common applications.

Edit
 ↓
Save
 ↓
Edit
 ↓
Undo

3. Supports Version History

Multiple states can be stored.

Version 1
Version 2
Version 3
Version 4

4. Simplifies State Restoration

The Originator itself knows how to restore its state.


5. Works Well with Immutable Snapshots

C# records and immutable objects make Memento implementations safer.


6. Separates State Management

The Caretaker can manage history without understanding business internals.


29. Disadvantages of Memento Pattern

1. Memory Usage

Multiple snapshots can consume significant memory.


2. Storage Requirements

Persistent version history can require substantial database storage.


3. Performance

Creating snapshots of large objects can be expensive.


4. Complex Object Graphs

If an object contains nested objects:

Order
 ├── Customer
 ├── Items
 │    ├── Product
 │    └── Product
 └── Payment

creating a safe snapshot may require careful deep-copy logic.


5. Version Compatibility

If snapshots are persisted for a long time, changes to the object model can make older snapshots difficult to restore.


30. Best Practices

1. Prefer Immutable Mementos

Use:

public record DocumentMemento(
    string Content);

where appropriate.


2. Keep Memento Creation Inside the Originator

Prefer:

document.Save();

instead of allowing external classes to manually copy every field.


3. Don't Expose Internal State Unnecessarily

The Caretaker should not need to manipulate the Originator's fields.


4. Limit History Size

For example:

private const int MaxHistory = 50;

This prevents unbounded memory growth.


5. Consider Deep Copy Carefully

If the state contains mutable objects, simply copying references can create unexpected behavior.


6. Use Database Versioning for Persistent History

If version history must survive application restarts, storing snapshots in memory is insufficient.


7. Consider Differential Snapshots

Instead of saving the entire object every time:

Version 1 → Full Snapshot
Version 2 → Changes only
Version 3 → Changes only

This can reduce storage requirements.


31. Common Mistakes

Mistake 1 – Using Shallow Copies Accidentally

Consider:

public class Order
{
    public List<OrderItem> Items { get; set; }
}

Copying the Order object without copying the list and its elements can leave multiple versions sharing the same mutable objects.

For state restoration, this can cause bugs.


Mistake 2 – Storing Mutable Mementos

A memento should represent a stable point-in-time state.

Prefer immutable state.


Mistake 3 – Unlimited History

This can eventually consume a large amount of memory.


Mistake 4 – Using Memento Instead of Transactions

Memento does not replace database transactions.


Mistake 5 – Saving Huge Objects Too Frequently

For example:

10 MB object
+
1000 snapshots
=
10 GB

This is a simplified example, but it illustrates why snapshot size and history limits matter.


Mistake 6 – Exposing All State to the Caretaker

If the caretaker understands every field, encapsulation has effectively been lost.


32. Real-World Enterprise Scenarios

The Memento Pattern can be useful for:

Document Management

Document
 ↓
Version
 ↓
Restore

Configuration Management

Configuration
 ↓
Snapshot
 ↓
Apply Change
 ↓
Rollback

Workflow Systems

Workflow State
 ↓
Snapshot
 ↓
Continue
 ↓
Restore

Game Development

Game State
 ↓
Save Point
 ↓
Continue

Drawing Applications

Canvas
 ↓
Snapshot
 ↓
Edit
 ↓
Undo

Form Editors

Form State
 ↓
Save
 ↓
Modify
 ↓
Undo

Business Applications

Business Object
 ↓
Snapshot
 ↓
Business Operation
 ↓
Restore

33. Memento in Enterprise Architecture

A simplified architecture might look like:

                    API
                     |
                     ↓
                Application
                     |
                     ↓
                Domain Object
                     |
               +-----+-----+
               |           |
             Save        Restore
               |           |
               ↓           ↓
            Memento ←→ History
               |
               ↓
        Persistence Store

This approach can be particularly useful when an application requires explicit state/version management.


34. Memento with Dependency Injection

Memento itself doesn't require dependency injection.

However, in ASP.NET Core, a history or snapshot repository can be injected.

For example:

public interface IDocumentHistoryRepository
{
    Task SaveAsync(
        int documentId,
        DocumentSnapshot snapshot);

    Task<DocumentSnapshot?> GetPreviousAsync(
        int documentId);
}

Implementation:

public class DocumentHistoryRepository
    : IDocumentHistoryRepository
{
    public Task SaveAsync(
        int documentId,
        DocumentSnapshot snapshot)
    {
        // Save snapshot to database

        return Task.CompletedTask;
    }

    public Task<DocumentSnapshot?> GetPreviousAsync(
        int documentId)
    {
        // Retrieve previous snapshot

        return Task.FromResult<DocumentSnapshot?>(
            null);
    }
}

The application service can then use the repository.


35. Memento and Clean Architecture

Memento can fit naturally into a Clean Architecture solution.

For example:

MyApplication
│
├── API
│
├── Application
│   ├── Commands
│   ├── Queries
│   └── Services
│
├── Domain
│   ├── Entities
│   ├── ValueObjects
│   └── Mementos
│
└── Infrastructure
    ├── Persistence
    └── Repositories

The exact placement depends on whether the snapshot is considered domain state, application state, or infrastructure persistence.


36. Interview Questions

Beginner

1. What is the Memento Design Pattern?

It is a behavioral pattern that allows an object to save and restore its previous state without exposing its internal implementation.


2. What are the three major components?

Originator
Memento
Caretaker

3. What is an Originator?

The object whose state needs to be saved and restored.


4. What is a Memento?

An object that stores a snapshot of the Originator's state.


5. What is a Caretaker?

An object responsible for storing and managing Mementos.


Intermediate

6. Where is Memento commonly used?

Examples include:

  • Undo/Redo

  • Document versioning

  • Configuration rollback

  • Game save points

  • Workflow state restoration


7. How does Memento preserve encapsulation?

The Originator creates and restores its own state. The Caretaker stores the Memento without needing to know the internal representation of the Originator.


8. What is the relationship between Memento and Undo?

Memento provides the saved state required to restore the object to an earlier state.


9. What is the difference between Memento and Prototype?

Memento preserves state for restoration.

Prototype clones an existing object to create another object.


10. Can Memento support Redo?

Yes.

A common implementation uses:

Undo Stack
Redo Stack

37. Advanced Interview Questions

11. What are the disadvantages of Memento?

The primary concerns are:

  • Memory usage

  • Storage requirements

  • Snapshot creation cost

  • Deep-copy complexity

  • Version compatibility


12. Does Memento replace database transactions?

No.

Memento manages application/object state.

Database transactions provide atomicity and consistency for database operations.


13. What happens if the object contains mutable collections?

A shallow copy may preserve references to the same collection, causing multiple states to share mutable data.

A suitable deep-copy or immutable-state strategy may be required.


14. How would you optimize Memento for large objects?

Possible approaches include:

  • Differential snapshots

  • Compression

  • Version identifiers

  • Limited history

  • Persistent snapshots

  • Event sourcing

  • Periodic full snapshots with incremental changes


15. Why should a Memento ideally be immutable?

Because a snapshot should represent a stable point-in-time state.

If the snapshot can change after being stored, history becomes unpredictable.


16. How would you implement Memento in ASP.NET Core?

Possible approaches include:

Domain Object
     ↓
Create Snapshot
     ↓
Application Service
     ↓
Snapshot Repository
     ↓
Database

17. How does Memento relate to Event Sourcing?

Both can preserve historical state, but they work differently.

Memento typically stores snapshots.

Event Sourcing stores the sequence of events that caused state changes.

They can also be combined.


18. When would you choose Event Sourcing instead of Memento?

If the application needs a complete, auditable history of business events, Event Sourcing may be more appropriate.

If the main requirement is simply restoring previous object state, Memento may be simpler.


38. Memento vs Event Sourcing

This distinction is especially important in enterprise applications.

Memento

State 1
State 2
State 3

The system stores snapshots.

Event Sourcing

AccountCreated
MoneyDeposited
MoneyWithdrawn
AddressChanged

The system stores events.

Current state can be reconstructed by replaying events.

Therefore:

Memento
→ Save state

Event Sourcing
→ Save changes/events

39. Practical Example – Document Editor

Let's put everything together.

Suppose a user writes:

Hello

The application saves:

Memento 1

User changes it:

Hello World

Save:

Memento 2

User changes it:

Hello World!

Current state:

Hello World!

The user presses Undo.

Current
Hello World!
      ↓
Restore
      ↓
Hello World

Press Undo again:

Hello World
      ↓
Restore
      ↓
Hello

This is the fundamental idea behind Memento.


40. Key Takeaways

Remember these important points:

1. Memento is a Behavioral Design Pattern

It focuses on managing and restoring object state.

2. It has three major components

Originator
Memento
Caretaker

3. Originator owns the state

It creates and restores Mementos.

4. Caretaker manages history

It stores snapshots without understanding their internal details.

5. Memento supports Undo/Redo

Two stacks are commonly used:

Undo
Redo

6. Prefer immutable snapshots

C# records are often convenient for snapshot data.

7. Watch memory usage

Large snapshots and unlimited history can be expensive.

8. Memento doesn't replace transactions

Use database transactions for database atomicity.

9. Memento and Event Sourcing are different

Memento stores state snapshots.

Event Sourcing stores events.

10. Use it when state restoration is a real requirement

Don't introduce Memento merely because it is a design pattern.


Conclusion

The Memento Design Pattern provides a clean way to preserve and restore an object's state while keeping the object's internal implementation encapsulated.

The fundamental architecture is:

                Originator
                   |
                   | Save()
                   ↓
                Memento
                   ↑
                   |
               Caretaker
                   |
                   | Restore()
                   ↓
                Originator

Its most common applications include:

  • Undo/Redo

  • Document versioning

  • Configuration rollback

  • Workflow restoration

  • Game save points

  • State snapshots

  • Application-level rollback

In modern C#, immutable records make snapshot objects particularly convenient:

public record DocumentMemento(
    string Content);

And in enterprise ASP.NET Core applications, Memento can be combined with:

ASP.NET Core
      ↓
Clean Architecture
      ↓
Application Services
      ↓
Domain Objects
      ↓
Memento / Snapshots
      ↓
Persistence

The most important lesson is:

Memento allows an object to preserve a point-in-time snapshot of its state and restore that state later without exposing the object's internal implementation details.

When used carefully, it provides an elegant foundation for undo/redo, versioning, and state restoration. However, for large or persistent histories, developers should consider storage costs, immutable snapshots, differential snapshots, database versioning, or Event Sourcing.


🚀 Coming Up Next: Part 4.7 – Observer Design Pattern

In the next article, we'll explore the Observer Design Pattern, including:

  • What is the Observer Pattern?

  • Why do we need it?

  • Subject and Observer concepts

  • One-to-many dependency

  • UML Class Diagram

  • Complete C# Console Application

  • Events and Delegates in C#

  • IObservable<T> and IObserver<T>

  • ASP.NET Core implementation

  • Event-driven architecture

  • Notification systems

  • Stock price monitoring

  • Banking transaction notifications

  • Order status notifications

  • Real-world enterprise scenarios

  • Advantages and disadvantages

  • Best practices

  • Common mistakes

  • Observer vs Mediator

  • Observer vs Pub/Sub

  • Observer vs Event-Driven Architecture

  • Interview questions

The Observer Pattern is especially important in modern .NET development because it provides the conceptual foundation for C# events, notifications, reactive programming, UI updates, and event-driven systems.

Mediator Design Pattern

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

  1. Introduction

  2. What is the Mediator Design Pattern?

  3. Why Do We Need the Mediator Pattern?

  4. The Problem with Direct Object Communication

  5. Mediator Pattern Solution

  6. Mediator vs Direct Communication

  7. Mediator Pattern Structure

  8. UML Class Diagram

  9. Components of the Mediator Pattern

  10. Complete C# Console Application

  11. ASP.NET Core Implementation

  12. Dependency Injection with Mediator

  13. Request/Response Communication

  14. Command and Query Handlers

  15. Mediator and CQRS

  16. Banking Example

  17. E-Commerce Example

  18. Pipeline Behaviors

  19. Validation

  20. Logging

  21. Authorization

  22. Transaction Handling

  23. MediatR-Style Architecture

  24. Real-World Enterprise Scenarios

  25. Advantages

  26. Disadvantages

  27. Best Practices

  28. Common Mistakes

  29. Mediator vs Observer

  30. Mediator vs Facade

  31. Mediator vs Command

  32. Interview Questions

  33. Key Takeaways

  34. Conclusion

  35. 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.

MediatorObserver
Centralizes communicationBroadcasts notifications
Usually coordinates interactionsOne-to-many relationship
Participants communicate through mediatorObservers subscribe to subject
Focuses on coordinationFocuses 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.

MediatorFacade
Coordinates communication between objectsSimplifies access to a subsystem
Objects communicate through mediatorClient communicates with facade
Focuses on collaborationFocuses on simplification
Behavioral patternStructural 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.

Don't Copy

Protected by Copyscape Online Plagiarism Checker