Category: System design

  • High Concurrency Handling: What Happens When Thousands of Users Hit Your API at the Same Time?

    High Concurrency Handling: What Happens When Thousands of Users Hit Your API at the Same Time?

    Imagine your application is running smoothly.

    Then your company launches a flash sale.

    Or a marketing campaign goes live.

    Suddenly, thousands of users start sending requests to the same endpoint at the same time.

    The question is no longer:

    “Does my API work?”

    It’s:

    “Can my API still work under heavy load?”

    Handling high concurrency isn’t about a single optimization. It’s about building a system that remains reliable as traffic grows.

    Here are some techniques I rely on.


    1. Avoid Blocking Threads

    In ASP.NET Core, every blocked thread reduces your application’s ability to handle incoming requests.

    Prefer asynchronous APIs whenever possible.

    await repository.GetOrdersAsync();
    

    Using async and await allows the thread to serve other requests while waiting for I/O operations.


    2. Optimize Database Access

    The database is often the first bottleneck.

    Reduce unnecessary work by:

    • Selecting only the required columns.
    • Adding proper indexes.
    • Using AsNoTracking() for read-only queries.
    • Avoiding the N+1 query problem.

    A faster query means every request spends less time waiting.


    3. Cache Frequently Requested Data

    Not every request needs to reach the database.

    Cache data that changes infrequently using solutions such as Redis or the in-memory cache.

    Reducing database traffic significantly improves throughput.


    4. Limit Expensive Operations

    Some endpoints perform CPU-intensive or long-running work.

    Protect your application with:

    • Rate limiting.
    • Background processing.
    • Queues for non-critical tasks.

    This prevents a small number of requests from consuming all available resources.


    5. Use Optimistic Concurrency

    When multiple users update the same record simultaneously, conflicts can occur.

    EF Core supports optimistic concurrency using concurrency tokens such as RowVersion.

    This helps prevent accidental overwrites.


    6. Scale Horizontally

    Sometimes optimization isn’t enough.

    Run multiple application instances behind a load balancer so incoming requests are distributed across servers.

    This increases capacity while improving availability.


    7. Monitor Before You Optimize

    Use tools such as:

    • Grafana
    • Prometheus
    • Application Insights
    • OpenTelemetry

    Measure:

    • Response time
    • Error rate
    • CPU usage
    • Database latency
    • Throughput

    You can’t improve what you don’t measure.


    Final Thoughts

    High concurrency isn’t just about surviving traffic spikes.

    It’s about designing systems that continue to respond quickly, remain reliable, and protect shared resources under pressure.

    Performance isn’t achieved through one big optimization.

    It’s the result of many small engineering decisions working together.

  • Caching Strategies Every Backend Developer Should Know

    Caching Strategies Every Backend Developer Should Know

    A few years ago, I thought caching was simple.

    Need better performance?

    Just put it in the cache.

    Problem solved.

    But after working on production systems, I realized that caching isn’t just about making applications faster.

    It’s about choosing the right caching strategy.

    The wrong strategy can lead to stale data, unnecessary database calls, or even cache stampedes.

    Here are the four most common caching strategies every backend developer should know.


    1. Cache-Aside (Lazy Loading)

    This is the most commonly used strategy.

    How it works:

    1. Check the cache.
    2. If the data exists, return it.
    3. If not, retrieve it from the database.
    4. Store it in the cache for future requests.

    Best for:

    • Product catalogs
    • User profiles
    • Frequently read data

    Pros

    ✅ Simple to implement.

    ✅ Cache stores only requested data.

    Cons

    • The first request is always slower (cache miss).
    • Data can become stale if not invalidated properly.

    2. Read-Through

    Instead of the application talking directly to the database, the cache is responsible for loading missing data.

    The application only communicates with the cache.

    Best for:

    • Systems with high read traffic.
    • Centralized caching layers.

    Pros

    ✅ Simpler application code.

    ✅ Consistent cache behavior.

    Cons

    • Depends on cache provider support.

    3. Write-Through

    Whenever data is written, it’s stored in both the cache and the database.

    This keeps the cache synchronized.

    Best for:

    • Applications where data consistency is important.

    Pros

    ✅ Cache always contains the latest data.

    Cons

    • Write operations become slightly slower.

    4. Write-Behind (Write-Back)

    The application writes to the cache first.

    The cache updates the database asynchronously.

    Best for:

    • High-write workloads.
    • Logging.
    • Analytics.
    • Telemetry.

    Pros

    ✅ Very fast write performance.

    Cons

    • Risk of data loss if the cache fails before persisting changes.

    Which Strategy Should You Choose?

    ScenarioRecommended Strategy
    Read-heavy applications✅ Cache-Aside
    Centralized cache layer✅ Read-Through
    Strong consistency✅ Write-Through
    High write throughput✅ Write-Behind

    Final Thoughts

    Caching isn’t just about speed.

    It’s about balancing:

    • Performance
    • Consistency
    • Complexity
    • Scalability

    My biggest realization was this:

    The fastest application isn’t the one that caches everything. It’s the one that caches the right data using the right strategy.

    Choose your caching strategy based on your application’s requirements—not because it’s the most popular pattern.

  • I Thought I Understood async/await… Until I Asked One Simple Question

    I Thought I Understood async/await… Until I Asked One Simple Question

    A few days ago, I came across a post explaining async and await in C#. It talked about state machines, ThreadPool, asynchronous operations, and methods resuming after await.

    I read the entire post.

    Honestly…

    I understood almost nothing.

    Like many developers, I had a few assumptions in my head:

    • Does async create a new thread?
    • Does await stop the thread?
    • If the method pauses, what exactly is paused?
    • When execution resumes, why doesn’t it immediately execute the next line?
    • If another thread continues the work, how does the program know where to continue?

    The more I thought about it, the more confused I became.

    So I started asking questions.

    The first realization completely changed my understanding:

    async does not create a new thread.

    That surprised me.

    For years, I subconsciously associated async with “background thread.”

    It isn’t.

    async simply tells the compiler that the method contains asynchronous operations and can be paused and resumed.

    Then came the second realization:

    await pauses the method, not the thread.

    That sentence looks simple, but it took me a while to truly understand it.

    Imagine an ASP.NET Core API.

    A request arrives, and a ThreadPool thread starts executing your controller action.

    Eventually, the code reaches this line:

    await _repository.GetCustomerAsync();
    

    The database now needs a few seconds to respond.

    My original assumption was that the thread would just sit there waiting.

    Wrong.

    The thread is released back to the ThreadPool so it can serve another request.

    Your method remembers exactly where it stopped.

    When the database finishes, .NET picks an available thread and continues executing the method from the line after await.

    Another question immediately came to my mind.

    “If execution continues after await, shouldn’t the next line execute immediately?”

    Then I realized something I had completely overlooked.

    Consider this example:

    public async Task SaveCustomerAsync()
    {
        Console.WriteLine("Step 1");
    
        await SaveToDatabaseAsync();
    
        Console.WriteLine("Step 2");
    }
    
    public async Task SaveToDatabaseAsync()
    {
        await Task.Delay(5000);
    
        Console.WriteLine("Database Saved");
    }
    

    Initially, I expected the output to be:

    Step 1
    Step 2
    Database Saved
    

    But that’s not what happens.

    The actual output is:

    Step 1
    Database Saved
    Step 2
    

    Why?

    Because SaveCustomerAsync() isn’t waiting for Task.Delay().

    It’s waiting for the entire SaveToDatabaseAsync() method to finish.

    Only after that method completes does execution continue after its own await.

    That was my “aha!” moment.

    Finally, I understood the role of threads.

    Threads don’t travel with your method.

    They simply execute whatever work is ready.

    When an await is reached:

    • The method pauses.
    • The current thread is released.
    • The asynchronous operation continues elsewhere (such as the database or operating system).
    • When the operation finishes, an available ThreadPool thread resumes the method.

    No thread sits idle.

    No CPU cycles are wasted waiting.

    That’s why async/await improves the scalability of applications like ASP.NET Core APIs.

    Looking back, I realized something important.

    Understanding async/await isn’t about memorizing definitions.

    It’s about asking the right questions.

    Sometimes the biggest breakthrough comes from admitting:

    “I don’t actually understand this yet.”

    And that’s perfectly okay.

    Every experienced developer has been there.

    If this post clears up the same confusion I had, then sharing my learning journey was worth it.

  • What If a User Uploads a Dangerous File to Your Server? Here’s How to Prevent It.

    What If a User Uploads a Dangerous File to Your Server? Here’s How to Prevent It.

    File upload is one of the most common features in web applications—but it’s also one of the most targeted by attackers.

    Imagine a user uploads a file named:

    invoice.pdf.exe
    

    Or renames a malicious executable to:

    resume.pdf
    

    If your application trusts only the file name or extension, you could expose your server to serious security risks.

    A secure file upload system should never rely on a single validation step.

    1. Validate the File Extension

    Allow only the file types your application actually needs.

    Example:

    • .pdf
    • .jpg
    • .png
    • .docx

    Reject everything else.

    Remember: This is your first line of defense—not your only one.


    2. Verify the MIME Type

    Don’t trust the extension alone.

    Check the file’s MIME type sent by the client.

    For example:

    • application/pdf
    • image/jpeg
    • image/png

    Keep in mind that MIME types can also be spoofed, so continue with additional checks.


    3. Validate the File Signature (Magic Numbers)

    The most reliable validation is checking the file’s binary signature.

    For example:

    • PDF → %PDF
    • PNG → 89 50 4E 47
    • JPEG → FF D8 FF

    If the file signature doesn’t match the expected format, reject the upload.


    4. Limit File Size

    Prevent attackers from uploading extremely large files.

    Example:

    • Images: 5 MB
    • Documents: 20 MB

    This reduces the risk of storage abuse and denial-of-service attacks.


    5. Rename Uploaded Files

    Never store files using the original filename.

    Instead, generate a unique name.

    Example:

    3b8b2d1d-ef7d-4db2-8a8c-0f5d91a0f4a7.pdf
    

    This avoids filename collisions and reduces information disclosure.


    6. Store Files Outside the Web Root

    Avoid storing uploaded files in directories that can execute code.

    Instead:

    • Store files outside the web root.
    • Serve them through a controller or API after authorization.

    This prevents direct execution of uploaded files.


    7. Scan for Malware

    Integrate an antivirus solution to scan uploaded files before making them available.

    This is especially important for systems that accept documents from external users.


    8. Restrict File Permissions

    Uploaded files should never have execute permissions.

    Grant only the minimum permissions required to read or write the file.


    9. Authorize Access

    Not every uploaded file should be publicly accessible.

    Always verify that the requesting user has permission to download or view the file.


    10. Log Upload Activity

    Record important details such as:

    • User ID
    • IP address
    • File name
    • File size
    • Upload time
    • Validation failures

    Logs help detect suspicious activity and support incident investigations.


    Final Thoughts

    Secure file uploads are about defense in depth.

    Don’t rely on a single validation.

    A robust upload pipeline should include:

    • Extension validation
    • MIME type verification
    • File signature checks
    • Size limits
    • Malware scanning
    • Secure storage
    • Proper authorization

    Security isn’t a single feature—it’s a combination of small decisions that work together to protect your application.

  • User বারবার Send Button চাপল, আর Customer-এর টাকা দুইবার কেটে গেল! এটা কীভাবে প্রতিরোধ করবেন?

    User বারবার Send Button চাপল, আর Customer-এর টাকা দুইবার কেটে গেল! এটা কীভাবে প্রতিরোধ করবেন?

    ধরুন, আপনি একটি Payment API তৈরি করেছেন।

    একজন User “Pay Now” বাটনে চাপ দিলেন।

    কিন্তু Server একটু ধীর (Slow)।

    User ভাবলেন,

    “হয়তো Click হয়নি!”

    তিনি আবার Button চাপলেন।

    তারপর আবার।

    কয়েক সেকেন্ড পরে…

    Customer Support থেকে ফোন—

    “একই Payment-এর জন্য Customer-এর টাকা দুইবার কেটে গেছে!”

    এখন প্রশ্ন হলো—

    সমস্যাটা User-এর, নাকি System-এর?

    আমার মতে, System-এর।

    কারণ একটি ভালো Payment System কখনোই একই Payment একাধিকবার Process করবে না।


    প্রথম প্রতিরক্ষা: Frontend

    Frontend থেকেই Duplicate Request কমানো যায়।

    যেমন—

    • Button Click করার পর Disable করে দেওয়া।
    • Loading Spinner দেখানো।
    • Request Complete না হওয়া পর্যন্ত দ্বিতীয় Click Block করা।

    এতে অনেক Duplicate Request শুরুতেই বন্ধ হয়ে যায়।

    কিন্তু…

    শুধু Frontend-এর উপর নির্ভর করা যাবে না।

    কারণ—

    • User Page Refresh করতে পারে।
    • Network Retry হতে পারে।
    • Mobile App একই Request আবার পাঠাতে পারে।
    • কোনো Bot Request পাঠাতে পারে।

    তাই আসল নিরাপত্তা Backend-এ থাকতে হবে।


    দ্বিতীয় প্রতিরক্ষা: Idempotency Key

    প্রতিটি Payment Request-এর সাথে একটি Unique Id পাঠানো হয়।

    যেমন—

    Idempotency-Key: 7f4c9d...
    

    যদি একই Key আবার আসে,

    Server নতুন Payment Process করবে না।

    আগের Result-ই Return করবে।


    তৃতীয় প্রতিরক্ষা: Database Constraint

    Database-এ এমনভাবে Design করুন,

    যাতে একই Transaction Reference দুইবার Insert-ই করা না যায়।

    যেমন—

    • PaymentReference
    • OrderId
    • TransactionId

    এসবের উপর Unique Constraint থাকতে পারে।

    Database অনেক সময় শেষ নিরাপত্তা দেয়।


    চতুর্থ প্রতিরক্ষা: Transaction

    Payment-এর গুরুত্বপূর্ণ Operation-গুলো Transaction-এর মধ্যে করুন।

    যাতে মাঝপথে কোনো সমস্যা হলে Partial Data Save না হয়।


    পঞ্চম প্রতিরক্ষা: Distributed Lock (যেখানে প্রয়োজন)

    যদি Multiple Server একই Payment Process করতে পারে,

    তাহলে Redis Distributed Lock-এর মতো সমাধান ব্যবহার করা যেতে পারে।

    এতে একই সময়ে একটি Payment শুধুমাত্র একটি Server Process করবে।


    সবচেয়ে গুরুত্বপূর্ণ শিক্ষা

    একজন User একই Request একাধিকবার পাঠাতে পারেন।

    Network একই Request Retry করতে পারে।

    Gateway একই Callback আবার পাঠাতে পারে।

    এসবই স্বাভাবিক।

    অস্বাভাবিক হলো—

    System সেই Request-গুলোকে নতুন Payment হিসেবে ধরে আবার টাকা কেটে ফেলছে।

    একটি ভালো Payment System-এর লক্ষ্য হওয়া উচিত—

    “One business operation = One successful payment.”

    যতবার Request আসুক না কেন।