Building a scalable web application with ASP.NET Core and Azure - part 1

Building a scalable web application with ASP.NET Core and Azure - part 1

In this series of blog posts, we are going to build a scalable web application that can handle millions of users with Azure and .NET.
Why, you might ask, do we even have to think about scalability as developers and architects? Isn’t “the cloud” infinitely scalable?

While that might be true to some extent, it can quickly become very expensive to just throw more and more hardware at your project. And even if money is not an issue for you (we should talk! 😉), your app will still hit a limit somewhere if it’s not built for the right amount of scalability1.

That’s actually the first important point: what is the right amount depends on the project. It doesn’t make sense to build a system that can handle millions of users per second if you currently only have five customers. Uber, for example, started with a very simple stack and improved and rebuilt its system multiple times2.

So, we’ll start with a simple, somewhat “naive” implementation using the same techniques as a normal CRUD application. Then, we’re going to measure the scalability, identify performance bottlenecks, and try to improve things until we meet our goal.

  • Part 1 (this post) defines the scenario and requirements.
  • Part 2 describes the initial implementation.
  • Part 3 shows how we can test the scalability using Azure Load Testing and identify bottlenecks.
  • Part 4 improves the scalability by implementing caching and relieving the load on the database.
  • Part 5 discusses advanced patterns and how to implement them using Azure Cosmos DB.
  • Part 6 summarizes our journey.

The scenario - Scalection

2024 being a big voting year, we decided the scenario we wanted to implement is a fictive electronic voting system for the following reasons:

  • It’s obvious why a scalable solution is required
  • It’s an easily understandable problem that doesn’t require lots of explanation
  • The core feature (voting) is complex enough to provide some challenges, yet simple enough to implement in a few lines of code
A graphic of an electronic voting system

The project name is going to be Scalection.

Workflow

The basic workflow a user goes through consists of two actions:

  • Retrieving the election data (available parties and candidates for preferred votes)
  • Storing the vote (party and optional candidate)

Requirements

  • Obviously, every voter is only allowed to vote once in an election
  • Votes have to be stored in a way that does not allow tracing them back to the voter
  • Voters and votes need to be associated with an election district in order to provide fine-grained results after the election

Disclaimer: A real electronic voting system would, of course, have a lot of additional requirements considering security, fail-safety, fraud-protection, denial-of-service attacks, etc., but in order to keep the example simple, we do not consider these here. Our goal here is not to simulate a real voting application accurately but to provide an easily understandable use case for building a scalable web application.

Assumptions

Authentication

Authentication is not part of this application. In a real system, it would make sense to use an existing digital authentication system like ID Austria. We’re going to assume that such a system provides us with a voter-id and associated election-district-id. In the real world, a JSON Web Token would probably be used to store such claims, but in order to keep the sample simple and easily testable, we’re just going to pass them as HTTP headers.

User interface

We’re not going to build a user interface. Since the focus of this series is building a scalable application, we’re only implementing the server part (“backend”) of the application in the form of a REST API.

HTTP API

This is how a user might interact with our application:

Request the parties and candidates for election with id af555808-063a-4eeb-9eb2-77090a2bff42:

GET /election/af555808-063a-4eeb-9eb2-77090a2bff42/party

Response:

200 OK
Content-Type: application/json
Body:
[
    {
        "partyId": "c77948df2-a387-5efd-936a-9324a753c6e1",
        "name": "Team Tabs",
        "candidates": [
            {
                "candidateId": "2996fe75-3a54-5bc8-b00b-3701cb494331",
                "name": "Brian Kernighan"
            },
            {
                "candidateId": "504c1141-ee13-52fd-ac5c-f32bfe31cca6",
                "name": "Dennis Ritchie"
            },
            ...
        ]
    },
    {
        "partyId": "17c6ba8d-7e8b-5533-bb6a-485369336359",
        "name": "Spaces Squad",
        "candidates": [
            ...
        ]
    }
    ...
]

Vote for a party and candidate:

POST /election/af555808-063a-4eeb-9eb2-77090a2bff42/vote
Content-Type: application/json
x-voter-id: 4711
x-election-district-id: 4712
{
  "partyId": "c77948df2-a387-5efd-936a-9324a753c6e1",
  "candidateId": "2996fe75-3a54-5bc8-b00b-3701cb494331"
}

Response:

204 No Content

Scalability goal

Before you can start thinking about scalability, you have to know what you’re aiming for. You need to answer questions like:

  • How many users are there (total and concurrent)?
  • How will the load be distributed over time?
  • How many requests does a typical user send?
  • What’s an acceptable response time?
  • Do we have to handle too much load gracefully (e.g. by returning appropriate HTTP status codes instead of errors or timeouts)?

For our example, let’s suppose we want to support up to 20 million voters per hour. That’s about 333k per minute or 5.5k per second. As our typical user will send two requests, our system will need to handle 11k requests per second.

Austria currently has about 6.4 million eligible voters (we could also turn this around and record that if all of them wanted to vote within 20 minutes, the system would be able to handle it). So, we could probably get away with a less scalable solution. On the other hand, with a critical system like this, it’s better to be safe than sorry. And who knows, maybe we can sell our solution to other countries as well… 😉

Summary

In this post, we defined the scenario and requirements for our Scalection app as well as a scalability goal. In the next post, we’ll start implementing a solution using our “standard” toolset ASP.NET Core, Azure SQL and Entity Framework.


  1. For example, while you can easily add multiple web servers to handle more requests, the database will become a bottleneck in many projects because you can’t just add more databases without compromising data integrity. ↩︎

  2. At the time of writing, the images seem to be broken on highscalability.com. A version of the post with working images is also available on LinkedIn↩︎