Xw3qBlog logoXw3qBlog

← Back to Home

Microservices Performance from a Full-Stack View: Lessons from a SaaS

20 views·Like (0)·0 comments
Full-Stack Microservices Performance: Lessons from an Online SaaS

A practical review of performance decisions in a multi-tenant online assessment SaaS, covering the gateway, authentication, data access, frontend, and judging flow. Useful for developers working with .NET, Next.js, and microservices


Microservices Performance from a Full-Stack View: Lessons from a SaaS Project

After building a multi-tenant online assessment system that could actually go live and run in production, one thing became very clear to me:

Performance optimization is not a final patch before release. It is a series of trade-offs that should be considered throughout the whole architecture.

This system was built for GESP / CSP programming exam practice and mock testing.

It includes a public website, a student portal, an admin system, authentication, business services, task scheduling, judging nodes, PostgreSQL, Redis, and a message queue.

The request path is not short.

This article is not a full project walkthrough. Instead, I want to focus on a few performance principles that I kept checking during development.

These are also the lessons I would most like to talk about when discussing full-stack or microservices work.

1. First Decide What Must Be Fast and What Can Be Slower

Online exam systems have a very clear traffic pattern.

The biggest traffic usually appears around a few moments:

  • when an exam starts
  • when many students submit at the same time
  • when users keep checking scores or reports

Not every page needs extremely low latency.

Because of this, I separated the system into two types of paths from the beginning.

Synchronous hot paths

These include:

  • login validation
  • exam eligibility checks
  • loading exam questions
  • saving exam submissions

These operations must be stable and fast.

Asynchronous cold paths

These include:

  • some notifications
  • statistics processing
  • background reports
  • admin batch jobs

If a task can go into a queue, I do not want it to block the user's request.

A lot of performance problems happen because hot and cold paths are mixed together.

So the first step is often not adding cache.

It is:

Move work out of the request when the user does not need to wait for it.

2. Gateway and Service Boundaries: Every Extra Call Adds Uncertainty

The backend uses a YARP gateway as the single entry point, with separate authentication, business, and support services.

Splitting services makes the architecture cleaner.

But there is a cost.

Every internal HTTP call adds latency, timeout risk, retry logic, and another possible failure point.

In practice, I try to follow three rules.

First, the browser only talks to the gateway.

The frontend does not call every microservice directly.

This keeps CORS, authentication, and connection handling simpler.

Second, cross-service calls should be short and clear.

For example, authentication-related data such as user benefits or invitation relationships should be handled through clear internal APIs.

I do not want one business service searching across several other services just to answer one simple question.

Third, if some user context can safely be carried in the gateway or token, I do not query the user database again.

JWT claims are not the answer to everything.

But when used correctly, they can remove many repeated "Who is this user?" database calls.

In microservices, performance is often not only about how fast one API is.

It is also about:

How long is the full call chain?

3. Multi-Tenant Architecture Is More Than Adding a TenantId

This system uses database isolation by tenant.

The platform database stores identity and tenant metadata.

Business databases are separated by tenant.

In this kind of SaaS architecture, performance problems are often not caused by ugly SQL.

They often come from connection handling and tenant routing.

There are three things I pay close attention to.

First, tenant connection information should be resolved and cached.

I use memory and Redis where appropriate, instead of discovering or rebuilding tenant database information on every request.

Second, read-only Entity Framework queries should usually use AsNoTracking().

List APIs should use pagination.

Admin pages should not try to load everything at once.

Third, indexes should be designed together with the real query patterns.

I do not want to wait until production becomes slow before thinking about indexes.

Fields such as these are common examples:

  • phone number
  • invitation code
  • OpenId
  • exam paper ID
  • exam session ID

In a multi-tenant system, correctness and performance are closely connected.

Isolation must be correct first.

If tenant isolation is wrong, later performance tuning is like fixing the engine while the boat is still leaking.

4. Redis Is Not a Universal Performance Button

Redis is used in many places in this system:

  • SMS verification codes
  • login tickets
  • session versions
  • tenant connection cache

But the more I use Redis, the more careful I become.

Some data is a good fit for Redis.

For example:

  • short-lived data
  • frequently read state
  • data that can be rebuilt if lost

Other data should not be cached blindly.

For example:

  • whether an order is really paid
  • whether an exam has really been submitted

These are business facts that need strong consistency.

There is also another engineering detail that matters.

Every cache needs:

  • a TTL
  • a rate limit strategy when needed
  • an invalidation plan

Without these rules, we are not really removing complexity.

We may only move the pressure from the database into a cache that is harder to debug.

For me, the important question is not:

How many Redis keys do we have?

The better question is:

Does every cache have a clear boundary?

5. Frontend Performance: Users Notice It Before the Backend Graph Does

The frontend is built with Next.js.

The public website and admin system live in the same codebase.

My approach to frontend performance is very practical.

I do not want the first page to load like a huge dashboard if the user only needs a simple landing page.

Public pages should use static or semi-static rendering when possible.

I do not want to hydrate a large amount of unnecessary state.

Lists and question banks should use pagination.

When search or filter conditions change, requests should be controlled.

I do not want the frontend to send one API request for every single key press.

Authentication also matters.

I use sliding session renewal to reduce cases where a user suddenly gets a 401 in the middle of work.

This is not only a user experience issue.

Repeated login, repeated profile requests, and repeated permission loading also create hidden performance costs.

There is one full-stack lesson that is easy to miss:

A fast backend cannot save a frontend that keeps making unnecessary requests.

6. Judging and Heavy Work: Isolation Matters More Than Faster Code

A programming assessment system needs code judging.

Judging work is naturally heavy.

It can use a lot of:

  • CPU
  • memory
  • disk I/O

It can also be unpredictable.

My rule is simple:

Do not let heavy judging jobs compete with online APIs for the same resources.

Judging nodes are relatively independent and can scale horizontally.

The online services handle orchestration and state changes.

They do not compile and execute code inside normal API request threads.

This may look like an architecture decision.

But it is also a performance decision.

If heavy peak workloads are isolated, the main online system is much more stable.

7. Observability: If You Cannot See It, You Cannot Improve It

After using Aspire to run the local services and infrastructure together, I developed a simple habit.

Before optimizing anything, I ask:

Which part is actually slow?

Without traces, logs, and health checks, performance discussions can quickly become guesswork.

This becomes even more important after deployment with Docker Compose and a reverse proxy.

If one container is restarted, one dependency becomes unstable, or one service suddenly becomes slow, I want to find it quickly.

If we can locate the problem, we can optimize it.

If we cannot locate it, we are only guessing.

The Main Lessons I Took from This Project

If I had to reduce the whole project into a few reusable lessons, they would be:

  1. Separate hot paths and cold paths before thinking about caching and indexes.
  2. Microservices performance problems are often hidden in long call chains.
  3. Multi-tenant systems need both good isolation and good connection reuse.
  4. Redis is useful for state and hot data, but not for every business fact.
  5. Frontend request discipline is part of full-stack performance.
  6. Heavy computation should be isolated from the online request path.
  7. Without observability, there is no real next step in optimization.

Final Thoughts

This system includes authentication, multi-tenancy, mock exams, public conversion pages, membership benefits, and production deployment.

Performance optimization was never a separate milestone.

It was something I had to think about almost every day while writing APIs and building pages.

The question was always:

If the number of users becomes much larger next year, will this design still hold up?

I do not believe in one-time performance tuning that stays correct forever.

I believe more in making repeated trade-offs under real business limits and being able to explain why those trade-offs were made.

If you are also building full-stack microservices with .NET and a modern frontend, I hope some of these ideas are useful.

I would also be interested to hear what performance problems you have seen in multi-tenant systems or high-concurrency workloads.

---

This article is based on a multi-tenant online assessment SaaS project that I built independently. The main technology stack includes .NET Aspire, YARP, PostgreSQL, Redis, RabbitMQ, and Next.js.

About petercontinue

peter love study

Comments

Sign in to leave a comment.

  • No comments yet. Be the first to comment.