Database

DBMS

ANGULAR

Angular

DIFFERENCE

Difference

VIDEO

Videos

Introduction To System Design

System Design

Have you ever wondered how applications like Netflix, Amazon, or WhatsApp handle millions of users simultaneously without crashing? Behind every successful software application is a well-planned system design. Before writing a single line of code, software engineers need a clear blueprint that defines how different parts of the system will work together to meet both business and technical requirements.

What is System Design?

System Design is the blueprint of a software system. It is the process of defining a system's architecture, components, interfaces, data flow, and interactions to build a solution that is scalable, reliable, efficient, and easy to maintain.

A well-designed system:

  • Defines the overall architecture, components, interfaces, and data flow.
  • Establishes clear boundaries between users, services, databases, and external systems.
  • Converts business requirements into a practical technical architecture.
  • Focuses on key quality attributes such as scalability, reliability, performance, security, and maintainability.
  • Balances business functionality with real-world operational challenges such as increasing traffic, system failures, cost, and future growth.

In simple terms, system design is the bridge between business requirements and implementation. It provides a roadmap that helps developers build software that not only works today but can also scale and evolve as user demand grows.

Why Is System Design Important?

System design is much more than an interview topic; it's a fundamental skill for building software that performs well in the real world. As applications grow, a good design helps them remain scalable, reliable, and easy to maintain.

Here are some key reasons why system design is important:

  • Scalability & Reliability: Build systems that can support millions of users while maintaining high availability and minimizing failures.
  • Architectural Thinking: Learn to make informed design decisions by understanding trade-offs such as the CAP Theorem, SQL vs. NoSQL, and consistency vs. performance.
  • Career Growth: Strong system design skills are essential for progressing into senior engineering, technical lead, and software architect roles.
  • Real-World Problem Solving: Design systems that solve actual business challenges instead of focusing only on writing code.
  • Better Decision Making: Evaluate trade-offs between scalability, cost, performance, development speed, and system complexity to choose the best solution.
  • Future-Proof Architecture: Design software that can evolve over time, making it easier to add new features, handle increased traffic, and avoid costly bottlenecks.
Don't learn System Design just to pass interviews. Learn it to build software that lasts. When you truly understand system design principles, you'll naturally perform better in interviews—but more importantly, you'll gain the ability to design scalable, reliable, and efficient systems that solve real business problems.

The Evolution of System Design Over the Last 25 Years.

System design has evolved dramatically over the past two decades. As user expectations, internet adoption, and computing power have grown, software architectures have transformed from simple monolithic applications into highly distributed, cloud-native systems capable of serving billions of users worldwide.

1995–2005: The Era of Monolithic Web Applications.

In the early days of the web, most applications followed a monolithic architecture, where the entire application was built and deployed as a single unit.

Key characteristics:

  • The LAMP Stack (Linux, Apache, MySQL, PHP) dominated web development.
  • Applications typically ran on a single server with a single relational database.
  • Basic MVC (Model-View-Controller) architecture became popular.
  • Websites relied heavily on server-side rendering, with little to no real-time functionality.

2005–2010: The Rise of Distributed Systems

As companies like Facebook, Amazon, and YouTube experienced explosive growth, traditional monolithic systems struggled to keep up with increasing traffic.

Major innovations included:

  • Caching technologies such as Memcached and Redis reduced database load.
  • Content Delivery Networks (CDNs) improved global content delivery.
  • Database replication increased availability and fault tolerance.
  • Horizontal scaling and load balancers became standard practices.

2010–2015: The Cloud Revolution

Cloud computing transformed how applications were built and deployed by making infrastructure available on demand.

Key developments:

  • Platforms like AWS, Microsoft Azure, and Google Cloud Platform (GCP) enabled elastic scaling.
  • Virtual machines and containers simplified application deployment.
  • NoSQL databases such as MongoDB and Apache Cassandra became popular for handling massive amounts of unstructured data.
  • Organizations began moving away from expensive on-premises infrastructure.

2015–2020: The Microservices Era

As software systems became larger and more complex, organizations shifted from monolithic applications to microservices for greater flexibility and faster development.

Key trends:

  • Applications were split into smaller, independently deployable services.
  • API Gateways centralized routing, authentication, and rate limiting.
  • Event-driven architectures using Apache Kafka and RabbitMQ enabled asynchronous communication.
  • CI/CD pipelines automated testing and deployment, allowing teams to release software more frequently.

2020–Present: Real-Time, AI & Edge Computing

Modern applications prioritize real-time experiences, intelligent decision-making, and global performance.

Today's system design focuses on:

  • Low-latency applications such as live streaming, online gaming, and AI-powered recommendations.
  • Serverless computing for event-driven workloads.
  • Kubernetes for container orchestration and scalable deployments.
  • Edge computing to process data closer to users and reduce latency.
  • Strong emphasis on security, observability, compliance, and resilience in cloud-native architectures.

The evolution of system design reflects the changing demands of software applications. What began as simple single-server applications has evolved into globally distributed, cloud-native systems that leverage microservices, event-driven communication, AI, and edge computing. Understanding this evolution helps developers appreciate why modern architectural patterns exist and how they solve today's scalability, reliability, and performance challenges.

Power Of Two Using Recursion

Problem Statement: Given an integer n, return true if it is a power of 2. Otherwise, return false.

Example 1:
Input: n = 2
Output: true
Explanation: 2^1 = 2

Example 2:
Input: n = 16
Output: true
Explanation: 2^4 = 16

Example 3:
Input: n = 3
Output: false

Understanding the Recursive Idea.

A number is a power of two if we can keep dividing it by 2 and eventually reach 1.

Image Showing Power of 2

Since we reached 1, 16 is a power of two.

Now look at 12: 
12 / 2 = 6
6 / 2 = 3
3 is not divisible by 2, so we stop.

Therefore, 12 is not a power of two.

Breaking Down the Recursive Approach.

Step 1: Handle Negative Numbers and Zero.
The powers of two numbers are always positive.
1, 2, 4, 8, 16, 32....

Step 2: Base Case
Once recursive reaches 1, we know every previous division was valid. This is the stopping condition.

Step 3: Check Divisibility
A power of two must always be divisible by 2 until it reaches 1.
Example:
10 is divisible by 2
5 is divisible by 2

Step 4: Recursive Call
If the current number is even, divide it by 2 and solve the smaller problem.

C# Recursive Code Implementation.

public class Solution {
    public bool IsPowerOfTwo(int n) {
        //Step 1: Handle Negative Numbers and Zero
        if(n <= 0) return false;

        //Step 2: Base Case
        if(n == 1) return true;

        //Step 3: Check Divisibility
        if(n % 2 != 0) return false;

        //Step 4: Recursive Call
        return IsPowerOfTwo(n / 2);
    }
}

Dry Run Example: n = 16

Call 1: IsPowerOfTwo(16)
  • 16 > 0
  • 16 != 1
  • 16 % 2 == 0
Call 2: IsPowerOfTwo(8)
  • 8 > 0
  • 8 != 1
  • 8 % 2 == 0
Call 3: IsPowerOfTwo(4)
  • 4 > 0
  • 4 != 1
  • 4 % 2 == 0
Call 4: IsPowerOfTwo(2)
  • 2 > 0
  • 2 != 1
  • 2 % 2 == 0
Call 5: IsPowerOfTwo(1)
  • Now all previous calls return true.
  • Final Answer will also return true.

Time Complexity: Each recursive call divides n by 2, so the complexity will be O(log n)
Space Complexity: Recursive calls are stored on the call stack of maximum depth O(log n)

Caching in ASP.NET Core.

Caching is the technique of storing frequently accessed data in temporary fast storage so that future requests can be served faster without repeatedly calling a slow source like a database or external API.

👉 In simple words: Cache = Fast memory that avoids repeated expensive operations

Without caching:

  • Every request hits the database
  • Higher response time
  • Increased load on DB
  • Poor scalability

With caching:

  • Faster response time
  • Reduced database calls
  • Better scalability
  • Lower infrastructure cost

Imagine a restaurant menu:
  • ❌ Without cache → Chef cooks every dish from scratch every time
  • ✅ With cache → Popular dishes are pre-prepared and served instantly
That pre-prepared dish = Cache

How does caching work?
  1. Client requests data
  2. Application checks the cache first
  3. If found → return from cache (cache hit)
  4. If not found → fetch from DB, store in cache (cache miss)

Types of Caching in ASP.NET Core

There are two main types you must know as a .NET developer:
  • In-Memory Cache
  • Distributed Cache

In-Memory Caching.

In-Memory Cache stores data in the RAM of the application server.

  • Lives inside the application process
  • Very fast
  • Data is lost when the app restarts
How To Implement In-Memory Caching?

Step 1: Register Memory Cache.
builder.Services.AddMemoryCache();

Step 2: Inject IMemoryCache
using Microsoft.Extensions.Caching.Memory;

public class ProductService
{
    private readonly IMemoryCache _cache;

    public ProductService(IMemoryCache cache)
    {
        _cache = cache;
    }
}

Step 3: Cache Data Using Cache-Aside Pattern
public List<string> GetProducts()
{
    const string cacheKey = "products";

    if (!_cache.TryGetValue(cacheKey, out List<string> products))
    {
        // Simulate DB call
        products = GetProductsFromDatabase();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));

        _cache.Set(cacheKey, products, cacheOptions);
    }

    return products;
}
When to Use In-Memory Cache:
  • Small applications
  • Single server apps
  • Lookup / static data
  • Config values
  • Not suitable for load-balanced systems

Distributed Caching.

Distributed Cache stores data in a separate cache server shared across multiple application instances.
Common implementations:
  • Redis
  • SQL Server Cache
  • NCache
Let's understand in detail, with Redis as an example, because it is one of the most popular Caching System.

Redis is an in-memory distributed cache used to:
  • Share cached data across multiple app instances
  • Improve performance
  • Reduce database load
👉 Unlike in-memory cache, Redis works in load-balanced & microservice environments.

Prerequisites
  • Option 1: Run Redis using Docker (Port: 6379)
  • Option 2: Local Redis
Step 1: Install Required NuGet Packages
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Step 2: Configure Redis in ASP.NET Core
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration =
        "my-redis.xxxxxx.use1.cache.amazonaws.com:6379";

    options.InstanceName = "MyApp:";
});

Step 3: Use Redis via IDistributedCache (Service Layer).
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

public class ProductService
{
    private readonly IDistributedCache _cache;

    public ProductService(IDistributedCache cache)
    {
        _cache = cache;
    }

Step 4: Cache-Aside Pattern Implementation.
public async Task<List<Product>> GetProductsAsync()
{
    const string cacheKey = "products";

    var cachedData = await _cache.GetStringAsync(cacheKey);

    if (cachedData != null)
    {
        return JsonSerializer.Deserialize<List<Product>>(cachedData);
    }

    // Simulate DB call
    var products = GetProductsFromDatabase();

    var options = new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
        SlidingExpiration = TimeSpan.FromMinutes(2)
    };

    await _cache.SetStringAsync(
        cacheKey,
        JsonSerializer.Serialize(products),
        options
    );

    return products;
}

Step 5: Cache Invalidation
public async Task UpdateProductAsync(Product product)
{
    // Update DB logic

    await _cache.RemoveAsync("products");
}
Note: Caching without invalidation is a bug, not a feature.

How To Implement Logging in ASP.NET Core.

Logging is the process of recording application events so you can:

  • Debug issues
  • Monitor behavior
  • Trace requests
  • Audit actions in production

Without proper logging:

  • Bugs are hard to reproduce
  • Production failures are invisible
  • Root cause analysis becomes guesswork

ASP.NET Core has ILogger built in via Microsoft.Extensions.Logging. Supported Providers (by default)
  • Console
  • Debug
  • EventSource
  • Application Insights (Azure)
Rule: Never log everything as Information.

How To Use ILogger?

Step 1: Inject ILogger.
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;

    public OrderController(ILogger<OrderController> logger)
    {
        _logger = logger;
    }
}

Step 2: Log Messages
_logger.LogInformation("Order creation started");

_logger.LogWarning("Order {OrderId} has no items", orderId);

_logger.LogError(exception, "Failed to create order {OrderId}", orderId);

Good Option 1: Logging Exception Correctly.
catch (Exception ex)
{
    _logger.LogError(ex, "Error while processing order {OrderId}", orderId);
    throw;
}
👉 Always pass the exception object to preserve stthe ack trace.

Good Option 2: Logging in Middleware.
public async Task InvokeAsync(HttpContext context)
{
    _logger.LogInformation(
        "Request {Method} {Path}",
        context.Request.Method,
        context.Request.Path);

    await _next(context);

    _logger.LogInformation(
        "Response {StatusCode}",
        context.Response.StatusCode);
}

Built-in logging is good, but Serilog is preferred in production because it provides:
  • Rich structured logging
  • Multiple sinks (File, DB, Seq, Elastic)
  • Better performance & flexibility
Step 1: Install Packages
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Step 2: Configure Serilog in Program.cs
using Serilog;

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File("Logs/app-.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog();

Step 3: Use ILogger as Usual.
_logger.LogInformation("Invoice {InvoiceId} generated", invoiceId);
You still use ILoggerSerilog works behind the scenes.

Interview Answer: In ASP.NET Core, I use ILogger for application logging and Serilog for structured, production-grade logging. I follow proper log levels, structured messages, correlation IDs, and centralized log storage to ensure observability and easy debugging.

Implement Custom Global Exception Handling in ASP.NET Core.

Exception handling is one of the most important non-functional requirements (NFRs) in any backend application. In ASP.NET Core, poor exception handling leads to:
  • Application crashes
  • Inconsistent error responses
  • Security risks (stack traces exposed)
  • Difficult debugging and monitoring
To solve this, ASP.NET Core promotes Global Exception Handling, where all unhandled exceptions are caught at a single place, logged, and converted into a consistent HTTP response.

This article explains:
  • Why is global exception handling needed
  • How to implement a custom global exception middleware
  • Best practices used in production systems

Why Do We Need Global Exception Handling?

Before understanding Global Exception, we need to know what an exception is and why we need to handle it properly.

An exception is a runtime error that occurs when the application cannot continue normal execution.
Examples:
  • Database connection failure
  • Null reference access
  • Invalid user input
  • External API timeout

If exceptions are not handled properly, they can:
  • Crash the application
  • Leak sensitive information
  • Return inconsistent responses
  • Make debugging extremely difficult
Global Exception Handling is a centralized mechanism that:
  • Catches all unhandled exceptions in one place
  • Logs them consistently
  • Converts them into a standard HTTP response
  • Prevents sensitive details from reaching clients
Instead of handling errors locally in every controller or method, the application handles them globally.

Let's understand the benefits of Global Exception Handling in detail with an example:

Example 1: Error Message Without Global Exception Handler.
{
  "error": "Object reference not set to an instance of an object"
}

Worse Case:
System.NullReferenceException at OrderService.cs line 42
Issues
  • Internal code details exposed
  • Frontend doesn’t know how to handle different formats
  • Logs may be missing or incomplete
Example 2: Error Message With Global Exception Handler.
{
  "statusCode": 400,
  "message": "Invalid request",
  "traceId": "abc123"
}
APIs always return a consistent response.

Imagine a building security desk:
  • Without global handling → every room handles its own security
  • With global handling → one central security desk handles all issues
This is exactly how Global Exception Handling works.
Interview Answer: Global exception handling is needed to centrally catch all unhandled exceptions, return consistent and secure error responses, improve maintainability, and enable reliable logging without duplicating try–catch blocks across the application.

 How To Handle Exceptions in ASP.NET Core?

ASP.NET Core provides multiple ways to handle exceptions, but not all are equal. A senior .NET developer is expected to choose the right approach based on maintainability, security, and scalability.

1. Try-Catch Block in Controller: Handling exceptions inside each controller action using try–catch.

[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
    try
    {
        var product = _service.GetProduct(id);
        return Ok(product);
    }
    catch (Exception ex)
    {
        return StatusCode(500, ex.Message);
    }
}
Problem:
  • Code duplication: try–catch in every action
  • Inconsistent responses: Different error formats
  • Security risk: Exception messages exposed
  • Poor separation: Business + error handling mixed
  • Hard to maintain: Changes needed everywhere

2. UseExceptionHandler() Middleware: Built-in middleware provided by ASP.NET Core for basic global exception handling.
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsync("Something went wrong");
    });
});
Problem:
  • Limited customization
  • No exception type mapping
  • No structured response
  • Not ideal for APIs
3. Custom Exception Middleware: A custom middleware that intercepts all unhandled exceptions, logs them, and returns a standard response.

Step 1: Create Custom Exceptions.
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

Step 2: Create Error Response Model.
public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string TraceId { get; set; }
}

Step 3: Create Middleware
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception");

            int statusCode = ex switch
            {
                NotFoundException => 404,
                _ => 500
            };

            var response = new ErrorResponse
            {
                StatusCode = statusCode,
                Message = statusCode == 500 ? "Internal error" : ex.Message,
                TraceId = context.TraceIdentifier
            };

            context.Response.StatusCode = statusCode;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsJsonAsync(response);
        }
    }
}

Step 4: Register Middleware
app.UseMiddleware<GlobalExceptionMiddleware>();

4. IExceptionHandler: A new interface-based global exception handling mechanism introduced in .NET 7.

Step 1: Step 1: Implement IExceptionHandler.
public class GlobalExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken cancellationToken)
    {
        context.Response.StatusCode = exception switch
        {
            NotFoundException => 404,
            _ => 500
        };

        await context.Response.WriteAsJsonAsync(new
        {
            message = "Error occurred",
            traceId = context.TraceIdentifier
        }, cancellationToken);

        return true;
    }
}

Step 2: Register Service.
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Step 3: Enable Middleware.
app.UseExceptionHandler();

Benefits:
  • Clean & structured
  • Official Microsoft approach
  • Easy integration with ProblemDetails
  • Less boilerplate
Interview Answer: For production-grade ASP.NET Core APIs, I prefer custom global exception middleware because it gives full control over exception mapping, logging, and response structure. For newer .NET versions, IExceptionHandler is also a clean and modern alternative.

Observer Design Pattern in C#.

In modern software applications, it’s common to have multiple components that need to react whenever something changes, like a weather display updating when the temperature changes, or different modules getting notified when an order is placed. Managing these updates manually quickly becomes messy and tightly coupled. This is where the Observer Design Pattern shines. It provides a clean, event-driven way for one object to notify many others automatically, making applications flexible, maintainable, and scalable.

What is the Observer Design Pattern?

The Observer Design Pattern is a behavioral design pattern used when you need one object (the Subject) to automatically notify other objects (Observers) of changes to its state.

This creates a one-to-many relationship between objects, where the Subject keeps a list of its Observers and notifies them whenever something changes.

Simple Words: Observer Pattern allows an object to publish events and multiple subscribers (observers) to react automatically.

When Do We Need the Observer Design Pattern?

Without the Observer Pattern:

  • You would manually call update functions for all dependent objects.
  • Code becomes tightly coupled.
  • Adding or removing listeners requires modifying the Subject class.
  • Any change breaks the Open/Closed Principle.

With Observer Pattern:

  • The subject doesn’t need to know who is observing.
  • Observers subscribe/unsubscribe on their own.
  • Adding new observers requires zero changes in the Subject.
  • It's event-driven and loosely coupled.
Real-Life Analogy: Think of it like a YouTube channel is a subject, and Subscribers are Observers. Whenever a channel uploads a new video, the YouTube channel notifies all your subscribers automatically. The channel doesn’t care how many people subscribed or who they are. Subscribers get updates based on their interests. 
This is exactly how the Observer Pattern works.

Observer Design Pattern

Observer Design Pattern Example.

Here is a C# example demonstrating the Observer pattern:
namespace PracticeCode.DesignPattern
{
    //Observer Interface
    public interface IObserver
    {
        void Update(float temperature);
    }
    //Subject Interface
    public interface ISubject
    {
        void RegisterObserver(IObserver observer);
        void RemoveObserver(IObserver observer);
        void NotifyObserver();
    }

    //Concrete Subject – Weather Station
    public class WeatherStation : ISubject
    {
        private List<IObserver> observers = new();
        private float temperature;

        public void RegisterObserver(IObserver observer)
        {
            observers.Add(observer);
        }
        public void RemoveObserver(IObserver observer)
        {
            observers.Remove(observer);
        }
        public void NotifyObserver()
        {
            foreach(var observer in observers)
            {
                observer.Update(temperature);
            }
        }

        //When temp change notify everyone
        public void SetTemperature(float newTemp)
        {
            Console.WriteLine($"\nWeatherStation: New Temperature = {newTemp}°C");
            temperature = newTemp;
            NotifyObserver();
        }
    }
    //Concrete Observers – Displays
    public class DigitalDisplay : IObserver
    {
        public void Update(float temperature)
        {
            Console.WriteLine($"Digital Display -> Updated Temperature: {temperature}°C");
        }
    }
    public class MobileDisplay : IObserver
    {
        public void Update(float temperature)
        {
            Console.WriteLine($"Mobile Display -> Updated Temperature: {temperature}");
        }
    }
}

Client Code in Program.cs, where the new observer is getting registered and getting notified whenever there is an update in temperature.
//Observers Subscribe
station.RegisterObserver(digital);
station.RegisterObserver(mobile);

//Observers Notify
station.SetTemperature(29.4f);
station.SetTemperature(30.2f);

//Observer Removed
station.RemoveObserver(digital);

//Observer Notify
station.SetTemperature(32.0f);
Output:
WeatherStation: New Temperature = 28.5°C
Digital DisplayUpdated Temperature: 28.5°C
Mobile AppTemperature Alert: 28.5°C

WeatherStation: New Temperature = 30.2°C
Digital DisplayUpdated Temperature: 30.2°C
Mobile AppTemperature Alert: 30.2°C

WeatherStation: New Temperature = 31.7°C
Digital DisplayUpdated Temperature: 31.7°C

This is one small example of how an Observer Design Pattern helps you write better and maintainable code, and in which all real-life conditions in which you can use this pattern.

These are some key points that you can keep in mind while writing an Observer Design Pattern Code:
  • Type: Behavioral
  • Purpose: Notify multiple objects automatically when one object changes.
  • Relationship: One-to-many
  • Helps With: Loose coupling, event-driven architecture
  • Key Methods: Register, Remove, Notify
  • Real Use Cases: Events, UI updates, stock market tickers, notifications
In Short, the Observer Pattern lets you build a system where one object publishes updates and many other objects automatically react to those updates.

DON'T MISS

Tech News
© all rights reserved
made with by AlgoLesson