Serverless Architecture
If you’ve been around long enough, you probably started building applications the “classic” way: create a single API, define a few endpoints, and deploy everything onto a server.
As systems grew, we split that monolith into multiple smaller APIs — microservices — each deployed independently, often across many servers.
But the idea doesn’t stop there.
Take it one step further, and you can break an application down into even smaller units — sometimes as small as a single endpoint — and deploy each piece independently.
Writing less code sounds nice, but you can easily end up spending more time deploying each piece than actually building the functionality.
Because of that, many cloud providers (like Azure) simplify DevOps for us by handling provisioning, scaling, execution, and resource cleanup on our behalf.
At this point, something interesting happens: you’re no longer thinking about servers at all. Not because they disappeared, but because you no longer have to care about them. This results in a serverless architecture.
Serverless architecture is a cloud computing model where developers build and run applications without managing servers.
So, How Exactly Does It Work?🤔
At the core of serverless is a simple idea: you write small pieces of code (a function), and something else decides when and where to run it.
The Function is a small, stateless unit of work. Each function is focused on doing one thing well, not hosting an entire application.
public class MyFunction
{
[FunctionName("MyFunction")]
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] // 👈 trigger
HttpRequest req)
{
. . .
}
}From there, the flow is straightforward:
- An event occurs (HTTP request, queue message, file upload, a scheduled job, or even a database change)
- The cloud platform invokes the corresponding function
- The function runs in an isolated environment
- Resources are allocated automatically for execution
- Once the function completes, everything is cleaned up
You don’t manage servers, scaling rules, or infrastructure. The platform handles it in real time.
Snap Back to Reality
I can already see some of you, inspired by “no servers at all” idea, but don’t rush up.
Functions work great for internal tools, background jobs, startups validating ideas, and event-driven systems. They let you move fast, focus on business logic, and avoid heavy DevOps work.
But they fall short when you need long-running processes, low-latency responses, or complex business logic.
Serverless is a great building block, but not always a great entire foundation.
Therefore, it is usually a bad idea to build an entire project on functions. Most real systems are a mix — a traditional monolith API with some serverless parts. Trying to force everything into serverless usually ends in messy code, painful debugging and tracing, hidden costs, and other problems we will discuss below.
Long-Running Execution Limits in Serverless
One of the most important constraints in serverless architecture is execution time. Even though you would like to move the background work to a function, serverless platforms are built for short-lived, event-driven workloads — not processes that run for minutes or hours. Usually, cloud platforms enforce a maximum execution duration, like up to 5–10 minutes (depending on plan).
Once this limit is reached, the function is terminated, leading the application to an inconsistent state and risking data integrity.
Because of these limits, long-running workflows must be redesigned:
- break work into small steps
- persist progress externally (database, queue, storage)
- make each step idempotent, and safe to retry
This isn’t just a technical limitation — it’s a shift in thinking. Serverless doesn’t support long-running execution. It forces you to model long-running workflows as a series of short tasks.
The Complexity Trap of Serverless
Serverless works best when the logic is simple, event-driven, loosely coupled, and short-lived.
At first glance, it may seem that serverless makes complex logic easier by breaking it into smaller pieces. That’s only partially true. Serverless doesn’t eliminate complexity — it redistributes it.
As business logic is split across multiple functions, it often becomes more fragmented and harder to read, reason about, and maintain. On top of that, serverless introduces additional challenges: coordinating between functions, managing state externally, and handling the overhead of distributed workflows.
— out-of-process by design
One important shift: your application is no longer stateful by default. Functions are ephemeral, so any state or in-memory data must be stored externally.
Therefore, you need to rely more on supporting services — databases, authentication providers, storage, and messaging systems — to persist and manage that state.
Of course, some cloud providers support durable functions, adding orchestration, state persistence, and automatic retries for long-running workflows in a way that’s transparent to the developer, but that does not completely eliminate the need for careful design.
The in-process mechanisms that worked for a single instance are no longer sufficient. Everything needs to be out-of-process now:
- to share cache between multiple nodes, it should be distributed
- threads synchronization can be achieved using the distributed lock
- configuration settings need to be consistent across instances
- task scheduling must ensure coordination across multiple nodes
- idempotent request coordination is a must
This result is a system with more moving parts — and, inevitably, more potential failure points.
— communication between components
In a monolith handling long-running processes, you can simply call one method after another within the same execution context. In serverless, that model breaks down — each step lives in a separate function, and you must explicitly pass context between them.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
Serverless components don’t communicate directly. Instead, they rely on event-based communication, where context is passed along with events from one function to the next.
If the context becomes too large to include in event payloads and you want to avoid document events, a common approach is to persist state in a database and use events only to signal that a step has completed and the next one should start.
While sharing storage is often considered an anti-pattern in microservices — where each service should own its data — in serverless architectures this is more acceptable. Functions are smaller, more transient units, and using shared storage as a coordination mechanism is often the most practical solution.
— transactions
In a monolith or a traditional service, complex operations often rely on a single transactional boundary. Multiple steps can be executed atomically — either everything succeeds, or everything is rolled back.
In serverless, that model no longer applies. There is no built-in way to wrap multiple steps in a single, atomic transaction.
To maintain consistency across steps, you end up implementing:
- compensating actions — manually undoing previously completed steps
- eventual consistency — accepting that the system may be temporarily inconsistent
- retry logic — handling partial failures and duplicate executions
What used to be straightforward transactional logic becomes real complexity.
Overall, if your domain logic is rich and tightly connected, a more traditional service (or even a modular monolith) is often the simpler, more maintainable choice.
Cold Start Problem in Lambdas
One of the most common downsides of serverless is the cold start.
A cold start happens when your function hasn’t been used for a while, so the platform has to deploy it before running. This does not happen instantly but requires time.
Although functions are mostly used for asynchronous execution when you don’t expect an immediate response, there are cases when it is not acceptable.
There are a few practical ways to reduce cold starts:
- keep functions small — less code, fewer dependencies, and simpler initialization = faster startup
- pay for pre-warmed instances — most providers let you keep some instances always ready (at an extra cost)
- deploy as a web app — a bit of a cheat 😅, but running as an app service removes cold starts entirely
- use warm-up strategies — trigger your function periodically (e.g., every few minutes) to keep it “warm”
- accept it where it doesn’t matter — background jobs, queues, async processing — cold starts are often irrelevant there. Don’t optimize what users never notice
Cold starts aren’t a dealbreaker — but ignoring them is a mistake. If you’re building anything user-facing, you need to account for them early, not after users start noticing delays.
Vendor lock-in
The last problem we're going to discuss is vendor lock-in.
Applications typically rely on standard protocols like HTTP to stay portable. This makes it possible to swap components, move them to different servers, or even rewrite parts of the system without having to rebuild everything from scratch.
Serverless is different.
Once you adopt it, you inevitably start depending on the Cloud platform itself. As you need to adapt to your cloud provider and how it handles:
- function runtime
- bindings (like Azure Functions input/output bindings)
- configuration and deployment style
- built-in integrations (storage, auth, messaging)
- etc
All of this is tightly coupled. Your function isn’t just “code” anymore — it’s code + platform.
Vendor lock-in is a problem developers try to deal with. The good news is, it is not a problem😅.
In practice, you will never change cloud providers. Aiming for a fully independent solution will make you lose most of the benefits and platform features, and you’ll spend a lot of time fighting the platform instead of using it.
That said, it doesn’t mean you should fall into the trap of putting everything inside a function. You still need to follow clean code principles to avoid ending up with messy, unmanageable code, like below, where you mix concerns:
public static class PlaceOrderFunction
{
[FunctionName("PlaceOrder")]
public static async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post")]
HttpRequest req,
// ⚠️ application logic mixed with infrastructure concerns
[ServiceBus(queueName: "orders-queue", Connection = "ServiceBusConnection")]
IAsyncCollector<OrderPlacedEvent> output)
{
// ⚠️ application logic mixed with infrastructure concerns
var userId = new UserId(Guid.Parse(req.HttpContext.User.Value)); // ⚠️
var order = new Order(OrderId.New(), userId);
dbContext.Orders.Add(order);
await dbContext.SaveChangesAsync();
await output.AddAsync(new OrderPlacedEvent(order.Id, userId)); // ⚠️
}
}Keep your business logic clean.
Isolate platform-dependent parts at the edges of your system, define application-level abstractions, and let the infrastructure layer handle cloud integration.
Don’t try to eliminate vendor lock-in entirely — manage it instead.
// Presentation Layer
public class PlaceOrderFunction
{
private readonly PlaceOrderUseCase _useCase;
public PlaceOrderFunction(PlaceOrderUseCase useCase)
{
_useCase = useCase;
}
[FunctionName("PlaceOrder")]
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post")]
HttpRequest req
// ✅ do not rely on Azure Functions–specific bindings in your core logic
/*IAsyncCollector<OrderPlacedEvent> output*/)
{
await _useCase.ExecuteAsync();
}
}
------------------------------------------------------------------------------------
// Application Layer
public class PlaceOrderUseCase
{
private readonly ICurrentUserService _currentUserService;
private readonly IDbContext _dbContext;
private readonly IMessageBus _messageBus;
public PlaceOrderUseCase(
ICurrentUserService currentUserService
IDbContext dbContext,
IMessageBus messageBus)
{
_currentUserService = currentUserService;
_dbContext = dbContext;
_messageBus = messageBus;
}
public async Task ExecuteAsync()
{
// ✅ application logic uses abstractions
var userId = _currentUserService.GetCurrentUserId(); // ✅
var order = new Order(OrderId.New(), userId);
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
await _messageBus.AddAsync(new OrderPlacedEvent(order.Id, userId)); // ✅
}
}From an architectural perspective, cloud-specific SDKs, configurations, and service integrations belong to the infrastructure layer and should be kept behind clear boundaries.
Treating the cloud as infrastructure preserves portability, keeps business logic clean, and prevents cloud providers from leaking into the core of your system. That way, even if you ever need to move, you’re not rewriting the whole system — just the parts that actually depend on the cloud.
Conclusion
- Serverless breaks applications into small, independently executed functions managed by the cloud.
- It works best for simple, event-driven, short-lived tasks — not complex or long-running processes.
- It excels at scalability, speed, and operational simplicity, but requires thoughtful design to avoid hidden complexity.
- Serverless does not eliminate the complexity of business logic — it forces you to split it into a smaller series of manageable steps.
- Additionally, you have to deal with orchestration, state management, communication, transactions, and consistency handling.
- Core limitations of serverless are timeouts, cold starts, and vendor lock-in.
- In practice, serverless is a powerful building block, but most real systems work best as a hybrid with traditional architectures.
- Used in the right context, serverless can significantly accelerate development and reduce costs.
That’s all folks 🐷 I hope you learned something new here 🤓 Give this article a clap 👏 Support me with coffee ☕️ And do not forget to follow to receive more ✅







