Common Backend Architecture Design

Published on
13 mins read
––– views

Monolithic Architecture

Monolithic refers to a mono design, meaning everything is in one place. All the necessary code, components, and services are bundled together.

It is one of the most popular architectural designs, and some companies operate solely on monolithic systems.

Here’s how a monolithic system works:

  • Users connect to your frontend.
  • The frontend connects to the backend.
  • The backend connects to the database.
  • The database returns data to the backend.
  • The backend returns data to the frontend.
  • The frontend displays data to users.

Most companies and projects start with this design because it addresses most initial requirements and solves early-stage problems. However, monolithic systems are tightly coupled, making them harder to modify and usually more difficult to scale. On the other hand, they allow for rapid development, as everything is centralized, making it easy to implement changes and see results quickly.

Monolithic systems are consistent. The entire system operates under a unified logic, and since everything is interconnected within the same system, they tend to be efficient.

Scaling a monolith involves vertical scaling—adding more resources such as RAM or storage. However, this approach has its limits, as monolithic systems cannot scale horizontally. For example, if one function becomes a bottleneck, you cannot add resources solely to that function; instead, you must scale the entire system, even if the other functions don’t require additional resources.

Imagine having ten different functions, and one starts to hit its limits. In a monolithic system, you’d need to scale the whole system to address the issue, which is inefficient.

Monoliths lack flexibility. A change in one component often requires redeploying the entire system. For large systems, this is a major drawback. For instance, imagine TikTok taking down its entire app for hours just to change an icon.

Monolithic systems are simple and easy to manage at the start, but as they grow, they can become overly complex and fragile. Many companies still run monolithic systems developed decades ago, with current teams having little to no understanding of their inner workings.

For example, at one of my previous jobs, we had a monolith from the early 1990s. The original developers were no longer with the company, and there was no documentation. It consisted of thousands of lines of code, and no one truly knew how it all worked. It was critical to the business, but we couldn’t take it offline or make changes. To make matters worse, over the years, countless developers had contributed to it, leading to inconsistent coding styles, libraries, dependencies, and rules. No two files looked the same, and the lack of consistency made it an unmanageable mess.

Monolith Use Cases

Monoliths are ideal for small-scale applications or startups where the app's scope is well-defined. They excel when the app is designed to solve one specific problem or perform one specific function. For example:

  • A hotel management app focused solely on managing hotels wouldn’t include features for dentistry clinics since that’s outside its scope.

Monoliths are great for entering the market quickly. Startups often begin with a single issue to solve. If they solve it and generate revenue, they can then look for new problems to tackle. A monolith allows for rapid development and deployment in these scenarios.

Performance-wise, monolithic systems are often faster than distributed systems because everything resides in one place. Distributed systems require data to travel between modules, increasing the number of requests and adding latency.

With a monolith, performance concerns are less critical during initial development. Even a 2/5 in code or design quality can yield acceptable performance. In contrast, distributed systems require more effort to optimize performance as part of the design process.

Distributed, (Service-oriented) Architecture

This design allows for continuous delivery of a large, complex apps. With this approach, you can build a service, add new features down the road, solve more problems, and scale the system as needed. When you login to a website with "coming-soon" or beta features, you are most likely a customer of a distributed system. Developer team is adding new features and services to the existing service and letting some users to beta test it, also called A/B testing.

Generic Services

These services can be part of a monolithic system, but they are also common in distributed systems. They are generic services designed to be used by multiple systems. For instance, you may have several distinct monolithic applications that all utilize the same payment service. This payment service is a shared, reusable component across multiple systems.

A common example of generic services is where you write your frontend and backend separately, with the backend communicating with the database.

Users interact with your frontend, and when they perform actions, an API call is triggered to the backend. The backend processes the request, communicates with the database, and returns the data to the frontend.

This setup might resemble a monolith but differs in an important way. Technologies like ASP.NET and Next.js support a monolithic approach, where both the markup (frontend) and backend logic are part of the same codebase. However, if you use ASP.NET for the backend and React for the frontend, you create a basic distributed system because you now have two separate codebases and deployments. Your frontend can remain operational even if the backend goes down.

Another advantage of this separation is that each team can maintain its own codebase, reducing the risk of breaking other parts of the system.

Additionally, if an attacker launches a DDoS attack on your frontend, the backend remains unaffected. In a monolithic setup, such an attack would impact the entire system.

Microservices Architecture

Most businesses think they have microservices, but in reality, they usually have generic distributed systems. To truly implement microservices, you generally need a large number of teams. For example, at Amazon, there are hundreds of teams, each owning one or two microservices. These microservices are loosely coupled and almost operate like independent products. Imagine it as ten different businesses coming together to accomplish a task far greater than any one of them could achieve alone.

If a microservice is handling multiple responsibilities, such as an entire business’s frontend, it’s not a microservice—it’s a generic service. A true microservice focuses on a single responsibility. When your service is designed to handle only one specific task, you potentially have a microservice.

Microservices operate independently. Unlike generic systems, they cannot be tightly coupled and they communicate with other services using protocols like HTTP, gRPC, or message queues (workers).

The typical evolution is for businesses to move from monolithic systems to distributed services and then to microservices. It’s very unlikely to transition directly from a monolith to microservices, as this requires massive growth and scale that most companies don’t achieve.

Microservice architecture, as the name suggests, consists of small, independent services distributed across multiple servers. Each service is responsible for a specific task. These modules are designed to be loosely coupled, allowing them to be developed, deployed, and scaled independently.

If you are going to reach out to another team for something, most likely you aren't in a microservices architecture, because it creates friction and dependencies. The point of microservices is to be independent and each team has their own product to manage.

For example, imagine a system with services A, B, and C. If service B fails, services A and C should still function without interruption. Another benefit is scalability—if service B becomes a bottleneck and requires more resources, you can scale only B, leaving A and C unchanged.

Consider an app where users can view pictures and videos, as well as like and comment on content. If the microservice handling likes fails, users can still view content and leave comments, but the like feature would be temporarily unavailable.

In contrast, if this were a monolithic system, the entire app would likely go down due to the failure of one component.

Additionally, video content usually requires more resources than pictures or comments. With a microservices approach, you can allocate more resources to the video service while assigning fewer resources to the comments and likes services.

In a monolithic system, you would have to scale the entire system, even if only the video functionality needed more resources.

Why even bother with distributed systems?

Independent development:

I don't know where you work, but where I work, if I need help from another team, and we do most of the time, it takes at least 2 to 3 days to get a reply. Because everything and everyone is so tighly coupled. We also have more layers of management than we have developers but thats just me yapping.

Teams can build their own product(service), focus on their own needs and not worry about other things as much as they would in a monolithic system. In a mono system, everything is a reason to worry. Will it break this, will it break that? Have you written tests for it (nobody writes tests).

Independent Deployment:

These can be deployed independently, when new features are pushed to production, teams don't need to get together. Its common to all development teams to get togather on specific days to deploy new features in order to minize errors. In monolith services, that is your life. Teams must come together and deploy everything at once. in distributed systems, If Architectured correctly, you wouldn't need to do that. There are still companies with distributed systems that needs to come together during the deployment days, but there are solutions like FBR (feature based releases).

Fault Isolation:

in a monolith, what happens if your backend goes down? Everything else goes down too. Let's say your new changes broke one of the 40 services in the cluster. Its not that big of a deal because other 39 are still running. In a mono setup, there is no way to only break one of the 40 services. They are all together in this mess.

Mixed Tech Stack:

Since every service is independent, it doesn’t matter if one team uses a language that you haven’t even heard of. Use Rust? Sure, I don’t care—I’m going to use Python. You’re using JavaScript? Bold choice, but I’ll stick with TypeScript.

In a monolithic setup, if your team writes in Python, the other team must also write in Python. If you write in Java, everyone else has to use Java too. Where I work, most of our apps are monoliths, so we write everything in C#. I also contribute to other projects part-time, where we use C# and React. This setup offers better developer experience because we’re not forced to use C# for frontend. We can pick different languages based on our needs. For the record, frontend with C# isn’t great—don’t let anyone tell you otherwise.

Complexity:

This approach adds complexity, and developers must be prepared to deal with it if they choose distributed systems. If one team adds something, another team adds something else, and no one takes ownership, it can quickly become a mess. Working with microservice architecture requires discipline and careful planning. You can’t just pile more things on top to make it work. Eventually, someone will have to support it—and that someone will likely be you or another unfortunate soul in your organization.

This version keeps your conversational tone while improving grammar and readability.

Testing

Writing tests becomes harder because you’ll have many more repos than you would in a monolithic setup. Each repository will require its own set of tests. Additionally, integration tests are often stored separately, likely in their own repositories.

For example, if you start with 50 repositories without tests, adding tests could bring you to around 100 repos. That’s a lot of repositories to manage and maintain.

Data consistency

Since each microservice handles a specific task and has its own database, maintaining consistent data schemas across services requires careful work and planning.

What if a request updates 2 out of 3 services, but the third one fails? Now, you have 2 databases with the same data and one without it. They are no longer in sync.

Imagine this scenario in a payment system: two databases show that a user has paid, but one says they haven't. Did the user actually pay or not? How do you ensure the data is consistent across all services in such cases?

Serverless Architecture

Serverless: one of the coolest innovations in recent software development history. The name itself sounds magical, like it’s powered by wizardry.

At its core, serverless means offloading your backend infrastructure concerns to a third party and letting them handle it for you. In exchange, you pay for the service—sometimes quite a lot—but it saves time and engineering effort.

Serverless is a cloud computing model where you rely heavily on third-party platforms like AWS, Firebase, Supabase, Vercel, or Google Cloud. These platforms may provide BaaS (Backend as a Service) or FaaS (Function as a Service), making "serverless" a broad, catch-all term.

How it Works:
The app logic still runs on servers, but these servers are managed by the cloud provider (e.g., Amazon, Google). You provide your code and logic, and the provider takes care of the infrastructure, scaling, and maintenance. For you, it's serverless.

Benefits:

  • Ease of Deployment: Serverless platforms simplify deployment. Drop your files into a bucket, and it handles the rest. If there's a build issue, most platforms prevent the changes from being pushed and notify you of the error—a helpful safety net against potential breakages.
  • Time Savings: Deployment speeds up since you skip staging and server adjustments. While this might only save 5–10 minutes per deployment, for 100 deployments, it adds up significantly.
  • Improved Developer Experience: Even as a solo developer, you can achieve what large teams could, thanks to managed infrastructure.
  • Performance: Companies like AWS deliver excellent speeds and load times. For example, large React bundles can load in just 1 second using serverless hosting, without performance tweaks.

How to Identify Serverless:
If you don’t manage the infrastructure and the experience is worry-free, then it’s serverless.

In fact, “serverless” might be a misnomer. Perhaps it should have been called “worryless,” as that better conveys the concept and avoids confusion.

Biggest problem with serverless is the cost. Serverless is expensive, it does offer great benefits but you do pay for those benefits. You pay for the usage, not a pre-purchased capacity. Which is great and not so great.

  • Great because you don’t have to pay for the capacity you don’t use.
  • Not so great because if you have a sudden spike in traffic, you’ll pay for that spike.

Serverless Scaling Risks

Serverless sounds amazing, but it has a dark side: scaling costs. While serverless solutions auto-scale effortlessly, your wallet doesn’t auto-scale.

Cost Surprises

  • Infinite Loops: A developer friend wrote a useEffect hook without dependencies, creating an infinite refresh. In less than a minute, his bill skyrocketed from $1–$2 to $300.
  • AI App Disaster: On X (Twitter), a developer shared how her AI app on Vercel generated a $300k bill. Vercel later reached out and resolved it, but such cases are a harsh wake-up call.

DDoS and Billing Protections

Until recently, AWS Lambda customers paid for DDoS traffic. While Amazon adjusted their pricing to address this, other platforms may still pass these costs to customers. Always research pricing policies for your chosen provider.

Cascading Lambda Calls

Serverless functions (e.g., AWS Lambda) can trigger each other in loops if designed poorly:

  1. Lambda calls another Lambda.
  2. That Lambda calls yet another.
  3. Repeat until your bill looks like your future kid’s college fund disappearing.

Mitigation Tips

  1. Set Usage Limits: Configure hard caps on billing and usage to avoid runaway costs.
  2. Test Your Code Thoroughly: Especially for infinite loops or recursive calls in serverless architectures.
  3. Monitor Billing Closely: Enable real-time alerts for unusual spikes.
  4. Plan Your System Design: Avoid unnecessary chain calls or poor architectural decisions.

One mistake in serverless can bankrupt you faster than a failed startup idea.