On-Premise, Cloud, or Serverless: What Problem Each One Solves

architecturecloudserverlessawsjava

There's a conversation that comes up every time a team starts a project or inherits one: "Do we move this to the cloud or keep it here?" It almost always drags a second one along with it: "What if we go serverless?"

It sounds like one question. It isn't. It bundles three separate decisions, and that's why the discussion stalls: everyone in the room is answering a different one.

In this article I pull those three decisions apart, and then I compare the three scenarios using the same application, so the only thing that changes is the infrastructure, not the business.

First: it's three decisions, not one

DecisionThe question it answers
Location and provisioningWhere does it run, and how do I get the resources?
Execution and operationsWho looks after the servers, the operating system, and capacity?
Software designHow do I split domains, transactions, and responsibilities?

On-premise tells you where the resources live: inside the company's environment. Cloud describes a model for consuming them on demand, and a private cloud can exist inside or outside the company's premises (NIST). When I say cloud here, I'm mostly comparing the public cloud with the company's own data center.

Serverless is a way of using the cloud, not a place. There are still servers; the provider runs them. And it doesn't only mean functions: Fargate runs containers without you managing the hosts (AWS).

The third decision, modular monolith, microservices, or events, works with either of the first two.

What to take with you: a Java monolith can run in the cloud, and a set of microservices can run on-premise. Changing how you deploy doesn't force you to split your domain.

The case I use to compare

A store with a React frontend, a Java backend, and order processing. The flow is the same in all three scenarios: the customer signs in, places an order, gets a response, and waits for an asynchronous confirmation.

Infographic: the same application deployed on-premise, in the cloud with EC2, and in serverless cloud, with its identity, entry, compute, data, and messaging components

I don't see this as a ladder you have to climb, from on-premise to containers to functions. Managed services widen your options; each workload may call for a different mix.

On-premise: local control, and you run it

This scenario exists for a specific need: running close to resources that have to stay inside the company. A plant, a low-latency network, specialized hardware, local integrations, or contractual constraints you've actually verified. Paid-off infrastructure and a team that can run it also count.

Here's the flow. React sends the login to Keycloak, then passes the access token to the entry proxy (NGINX with a WAF), which routes it to Spring Boot. The API checks identity and permissions, calculates the order, and writes the order and its event in a single PostgreSQL transaction. Another process publishes that event to RabbitMQ, and a Java worker handles the confirmation. The database is never exposed to the browser.

What you gain is control over the network, the hardware, maintenance windows, and where the data lives. What you take on is everything else: capacity, patches, certificates, storage, monitoring, and recovery. Two processes on the same server don't remove the risk of that server failing, and a backup is only worth something if you can restore it.

The usual mistake: assuming on-premise means "no autoscaling." You can have containers, Kubernetes, and infrastructure as code. The difference is that scaling needs resources that are already there: if the cluster is full, a policy won't create capacity you haven't bought yet.

I'd pick this option when being close to local systems, having control, or meeting verified constraints is worth more than handing off operations. I'd revisit it if the team keeps losing delivery time to infrastructure, or if peaks keep outgrowing what's installed. And keep in mind: a compliance requirement, on its own, doesn't prove the cloud is off the table.

Cloud with EC2: programmable capacity without rewriting your Java

This scenario covers a different need: deploying a Java API that runs continuously and adding resources without buying servers. You keep a model your team already knows, with long-running processes, containers, and a relational database. It's useful when you want to move or modernize a system gradually, without rewriting its logic as functions.

Cognito handles identity. A WAF and an ALB take the request, ECS schedules the containers on EC2 hosts, and Spring Boot processes the order. RDS PostgreSQL stores the data and the outbox; a relay publishes events to SQS and an ECS consumer processes them. The API and RDS can sit in private subnets while the public ALB only receives the traffic you allow.

This is where people get confused most: AWS runs the physical infrastructure, but with ECS on EC2 you still manage the hosts, their operating system, and the application. RDS and SQS hand off other layers. One solution mixes different levels of management (AWS, shared responsibility in ECS).

You keep the Java runtime, your diagnostic tools, and local transactions. You can scale tasks and capacity with policies, though provisioning and startup take time. And moving the code won't fix an N+1 or a missing index: latency still depends on your queries.

I'd choose EC2 with ECS if I need control over the hosts or have requirements that justify it. If I only need to run containers, I'd look at Fargate first: it's serverless too (AWS, Fargate vs. Lambda comparison).

Serverless with Lambda: event-driven, less infrastructure

This scenario lets you run logic without provisioning the servers each function runs on. It fits webhooks, integrations, file processing, and asynchronous work with variable demand. The point is to hand off the infrastructure and let capacity follow demand.

The customer signs in with Cognito and calls API Gateway with their access token. The authorizer verifies the JWT and a Java Lambda runs the logic; business authorization still lives in your code. DynamoDB records the order, Streams captures the change, and another Lambda publishes it to SQS, where a consumer processes it idempotently. Streams needs retries and failed-record handling configured (AWS, Lambda integration with DynamoDB Streams).

DynamoDB is a choice made for this example, not a requirement. You can keep a relational database if your data model needs it, as long as you design connections and concurrency so that scaling the functions doesn't overwhelm it.

Watch out: Lambda adds concurrent environments as demand grows, but within quotas and its scaling rate. Creating a new environment adds initialization latency, the well-known cold start. If you have a demanding SLO, measure both cases and consider provisioned concurrency or SnapStart where they apply (AWS, Lambda concurrency and lifecycle).

I'd use functions for bounded tasks, events, and variable demand when their limits fit. For long-running work or fine control over the process, I'd look at containers. And handing off the servers doesn't take IAM, dependencies, cost, observability, or data design off your plate.

What doesn't change: who's calling and what they can do

Wherever you move the backend, the security question stays the same: who's calling, what can they do, and on which data. In all three examples I use a React SPA with OpenID Connect on top of OAuth 2.0, using Authorization Code with PKCE (Auth0).

The ID token tells the client how authentication went. The access token is what you use to request access to the API it's meant for. They aren't interchangeable. And a SPA has no confidential client secret: its code ships to the browser.

The API checks the signature, the allowed algorithm, the issuer, the expiry, and the audience. The identity provider's public keys come from JWKS and can be cached, so validating a JWT doesn't mean calling the provider on every request. Spring Security ships Resource Server support for this pattern (Spring Security).

The usual mistake: treating authenticated as authorized. Validating the token tells you who someone is. Then your application decides whether they can create or read that order, looking at organization, owner, and state. That check can't rely only on having a gateway in front.

The same goes between services: in AWS, an IAM role can allow reading from SQS or DynamoDB, but it doesn't replace the user's identity or their permissions on an order.

What doesn't change either: distributed problems

A system is distributed when its parts work together over a network. Where it runs doesn't matter: even a monolith with a remote database already has distributed communication.

Don't lose the event. If the API saves the order and then publishes the message, a crash between the two leaves the order without a notification. With an outbox, the order and the event are written in the same local transaction, and a relay publishes what's been committed (AWS Prescriptive Guidance; Chris Richardson). But if the relay publishes and crashes before marking the event as sent, it publishes it again: the consumer has to tolerate duplicates. An outbox doesn't turn the database and the broker into a single transaction.

Put limits on failure. Timeouts per dependency and an overall latency budget. Bounded retries, with backoff and jitter, and only on idempotent operations. A queue absorbs work, but you have to watch its age and how fast it drains, and a DLQ needs a reprocessing procedure.

Measure the whole service. Measure P99 at the API's entry point and follow the request with traces down to the queries. An average hides initialization, slow queries, and saturation. And adding replicas doesn't fix an N+1.

Know how much you can lose. RTO is the longest acceptable time to restore the service; RPO is the largest window of data you're willing to lose. Multi-AZ helps with certain failures, but it doesn't replace a recovery plan you've tested (AWS Well-Architected).

How I choose between the three

There's no universal winner. First I rule out whatever breaks a real constraint; then I compare the cost and complexity of meeting the goals. This table shows tendencies, not scores:

CriterionOn-premiseCloud with EC2Serverless with Lambda
Local dependenciesAn advantage near internal equipment and systemsNeeds connectivity; measure latency back to local systemsConnectivity and its limits have to fit the flow
Load and capacityScales within what's installed, or by expanding itAdds hosts and tasks through policiesAdds concurrency within quotas; protect your dependencies
Process controlBroad: hardware, operating system, and runtimeHosts and runtime on the provider's resourcesLess; you design around the service
Team operationsPhysical infrastructure and platformHardware is handed off; hosts and app remainExecution is handed off; code, IAM, and config remain
Latency and durationDepend on network, capacity, and designCompute stays warm; validate P99Measure initialization and the mode's limits
PortabilityDepends on hardware and automationContainers move; managed services need adaptingIntegrations with provider APIs need adapting
Total costInvestment, idle capacity, power, and peopleProvisioned capacity, services, network, and operationsUsage, warm capacity, services, network, and operations

And the order in which I ask the questions:

  1. Constraints. Where the data and dependencies can live, what connectivity exists, and what has to keep working without internet.
  2. Load. Duration, concurrency, seasonality, and payload size.
  3. Goals. P99, availability, RTO, and RPO.
  4. Team. Real experience and on-call capacity.
  5. Evidence. A representative test, with costs and with failures.

To compare costs, I use the same load and the same SLO, and include compute, data, requests, transfer, network, logs, redundancy, and operating hours. Serverless can pay off with intermittent load; steady usage can favor something else. And "serverless" doesn't mean everything is billed per request: Fargate bills for the resources tasks request while they run (AWS, Fargate pricing model).

Three needs and a sensible mix

An inventory system next to plant equipment that has to keep working when the internet goes down. I'd keep the core on-premise and use the cloud for sync or analytics when the link is up. The proposal has to spell out how changes get reconciled once the connection comes back.

An orders API on Spring Boot and PostgreSQL, with steady traffic and a team used to containers. A modular monolith in containers is a reasonable foundation. I'd look at Fargate to hand off the hosts, and EC2 if control or a cost analysis justifies it. I'd keep the relational database: that way I avoid a data migration with no proven benefit.

Third-party webhooks that arrive in bursts, with long quiet stretches in between. An authenticated entry point, a queue, and Lambda consumers separate receiving from processing. Before accepting a task for good, I'd store it durably, with deduplication, concurrency limits, and a reprocessing flow.

Often the best answer is a mix: a local transactional core, APIs in the cloud, and events on serverless. What matters is deciding where each responsibility ends: which data is the source of truth, which communication is synchronous, and what can settle with eventual consistency. And I'd avoid having a user transaction hop across several environments when it doesn't need to, because every hop adds latency and new ways to fail.

Note: here's how I'd put it as an architect or tech manager: "We chose this combination because it meets these constraints and these SLOs. We accept these costs and risks. We'll track these indicators and revisit the decision if the load, the dependencies, or the team change." That's a proposal you can verify, not a preference for tools.

The sources are primary documentation, consulted on November 3, 2026. The three cases are examples to reason with, not benchmarks or prices: before you build, check each service's quotas, runtimes, and terms.

What about you? What was the real constraint that ended up deciding where your system runs: the data, the team, the load, or the cost? Let me know in the comments.

View Spanish version