← All insightsArchitecture

Scaling a legacy .NET application without a rewrite

A ten-year-old C# application on SQL Server can serve its reads from memory without a rewrite. Put an API in front of the database and move screens over one at a time.

4 min read

Picture the application: ten years old, written in C#, talking to SQL Server through a data access layer that has grown a little every quarter. It works, and customers depend on it. It is also slow at nine in the morning, when everyone logs in at once and the database server sits at high CPU while the pages crawl.

The usual advice arrives in two flavours. Rewrite it, ideally as a set of services built around one cloud vendor; or buy a bigger database server and hope the next size holds for another year. The first is a multi-year project with no new revenue in the meantime. The second is a bill that jumps at every size step, with a hard ceiling.

Where the load actually comes from

Before choosing a fix, look at the traffic. In most business applications of this age, the vast majority of database calls are small reads: a permission check, a customer lookup, a dropdown list, the same profile row loaded on every screen. Each one takes a few milliseconds. Multiplied by every user and every click, they consume the database's capacity long before the reports and the heavy writes do.

Those reads rarely change the data they touch. A customer's address is read thousands of times between edits. If the reads come from memory instead of from disk, the database is left with the writes and the genuinely complex queries, a fraction of what it handles today.

What goes in front of the database

The approach is to put an API (a service the application calls over the network, instead of calling the database directly) between the application and SQL Server. Behind that API sits Redis, a memory store that answers in about a millisecond. The API serves reads from Redis and passes writes straight through to the database, which remains the source of truth.

The part that makes this workable is keeping Redis truthful. A sync service watches SQL Server using change tracking, a built-in feature that lists which rows changed, and reads that list on a short interval. Every insert, update and delete in the database is carried across to Redis.

The API exposes typed entry points: Get, Insert, Update, Delete, GetRows and GetTotalRowCount, over REST and gRPC (two standard ways for programs to call a service over a network). It also serves joins and multi-field queries from Redis, not only lookups of one row by its identifier, because a real screen rarely needs just that.

Moving screens over one at a time

Nothing about this requires one large cutover. The existing application keeps working while migration happens screen by screen, starting with the read-heavy pages that hurt most.

  1. Pick one screen whose reads are frequent and whose data changes slowly, such as a customer summary or a product list.
  2. Replace the data access calls on that screen with calls to the API. The rest of the application is untouched.
  3. Ship it, watch the database load on that path drop, and confirm the numbers on the screen match.
  4. Repeat with the next screen. Stop whenever the database is comfortable, or continue until every read path is served from memory.

Writes can move at the same pace or stay on the old path for a while; either way they land in SQL Server, and the sync carries them into Redis. Because the old and new paths read the same truth, a half-migrated application is not a risk. It is the normal state for months.

What stays and what changes

What stays: the database, its schema (the layout of its tables), its stored data, the C# language, the hosting, the deployment pipeline and the reports that already run. What changes: the screens you choose to move now call an API instead of composing SQL, and the read load on the database falls with each one.

What you do not add is cache code. There is no time-to-live setting in the codebase, no invalidation logic, no list of keys to clear after a write. A common reaction from engineers who inspect a finished migration is that they cannot find where the caching happens, because it is not in their code.

The database keeps its job as the source of truth; it simply stops answering questions it has already answered a thousand times.

How the fallback keeps risk low

The obvious question is what happens when Redis is down. The answer is that reads fall back to the database automatically. The API notices Redis is unreachable and answers from SQL Server, with nothing to switch over. Read availability tracks the database, exactly as it does today.

This is what makes the incremental path safe. The worst case for a migrated screen is that it behaves like the unmigrated version and reads from the database; the new path is a faster one that steps aside when it cannot help.

Two honest limits. On SQL Server the cache is updated on a short interval rather than streamed, so a row read a moment after it is written may briefly show the previous value; for the few screens where that matters, read those rows through the database. And the sync needs a working connection to both the database and Redis, so both deserve ordinary production monitoring.

Where redfly fits

redfly is this pattern packaged: the API, the sync service and the automatic fallback, deployed into your own cloud account, run by us as a managed service, or installed on your own servers, starting with SQL Server. We set it up with design partners against their existing tables, one screen at a time.

Ready when you are

Stop reading. Start shipping.

Work with us as a design partner and see the difference on your own database.

redfly API + Sync Service · Licensed directly from redfly