Cloud Architecture Explained: Building Modern Applications

By Oleksandr Andrushchenko — Published on
0 Likes
0 Dislikes
Cloud Architecture Explained: Building Modern Applications
Cloud Architecture Explained: Building Modern Applications

Modern cloud architecture is less about moving servers into a data center owned by somebody else and more about designing applications around failure, elasticity, automation, and independently scalable components. Compute instances disappear, networks become unreliable, traffic changes quickly, and managed services introduce their own quotas and failure modes.

A production cloud system therefore needs clear boundaries between traffic management, application compute, state, asynchronous processing, and observability. The architecture should continue serving useful traffic when individual components fail and should scale without requiring every layer to grow at the same rate.

The most important architectural decision is not which cloud product to select. It is where state lives, how traffic flows, what can fail independently, and how the system recovers.

Table of Contents

Building Blocks of Cloud Architecture

A useful cloud architecture separates responsibilities into layers that can fail, deploy, and scale independently. A typical production application contains an edge layer, traffic distribution, compute, data stores, asynchronous processing, and an observability plane.

Clients
   |
   v
DNS / CDN / Edge
   |
   v
Load Balancer / API Gateway
   |
   +-----------------------+
   |                       |
   v                       v
Application Instance   Application Instance
   |                       |
   +-----------+-----------+
               |
       +-------+-------+
       |               |
       v               v
   Database          Cache
       |
       v
Object Storage

Application
    |
    v
Message Queue
    |
    +----------+----------+
    |                     |
    v                     v
 Worker                Worker

The edge layer terminates public traffic close to clients and may provide TLS termination, caching, request filtering, rate limiting, and protection against abusive traffic. Static content should normally avoid application servers entirely when it can be served from object storage through a CDN.

Traffic distribution routes requests only to healthy application instances. This layer also creates an important abstraction: clients communicate with a stable endpoint while the compute fleet behind it can continuously change.

Cloud Computing
Cloud Computing

Compute executes application logic. It may run on virtual machines, containers, managed application platforms, or serverless functions. The execution model affects startup latency, deployment control, scaling behavior, operational complexity, and cost. The next article in this series covers these trade-offs in depth: Virtual Machines vs Containers vs Serverless.

Stateful services store durable application data. Databases, object storage, distributed caches, and queues have very different durability and consistency guarantees. Treating them as interchangeable storage layers produces subtle reliability problems.

Layer Primary Responsibility Typical Scaling Model Important Failure Mode
CDN / Edge Cache and protect public traffic Distributed automatically Origin unavailable or stale content
Load Balancer Distribute requests Managed horizontal scaling Unhealthy targets or routing failure
Application Execute business logic Horizontal replicas Instance crash or resource exhaustion
Cache Reduce expensive reads Replication or sharding Eviction, hot keys, or cache outage
Database Durable structured state Vertical scaling, replicas, partitioning Primary failure, contention, replica lag
Queue Buffer asynchronous work Partitioning and consumer scaling Backlog growth or duplicate delivery
Object Storage Durable blobs and static assets Provider-managed Access, lifecycle, or dependency failure

The separation matters because different workloads scale differently. API traffic may double while database writes remain constant. Image uploads may increase storage without increasing CPU consumption. A large import may create millions of background jobs without changing synchronous request traffic.

A monolithic deployment can still use this architecture. Cloud architecture does not require microservices. Separating operational responsibilities is usually more important than splitting application code into many independently deployed services.

Designing the Request Path

The synchronous request path determines user-visible latency and availability. Every dependency added to that path contributes network latency and creates another opportunity for failure.

Consider an API that synchronously calls authentication, inventory, pricing, payment, analytics, and notification services before responding. Even when every dependency is individually reliable, their combined availability can become significantly lower than the availability of any single service.

Client
  |
  v
Edge
  |
  v
Load Balancer
  |
  v
API
  |
  +---- Cache
  |
  +---- Database
  |
  +---- Queue ----> Background Workers
  |
  v
Response

A healthy architecture keeps the critical path short. Work that does not determine the immediate response should usually be moved behind asynchronous boundaries.

Keeping Compute Stateless

Application instances should normally avoid owning durable request state. If session data, uploaded files, or job progress exists only on a particular instance, traffic becomes tied to that machine.

Instead, durable state should live in systems designed for it:

  • Relational or distributed databases for transactional business state.
  • Distributed caches for temporary shared state and frequently accessed data.
  • Object storage for files, images, exports, and large immutable objects.
  • Queues for asynchronous work awaiting processing.

This allows any healthy instance to process the next request.

from fastapi import FastAPI, UploadFile
from uuid import uuid4

app = FastAPI()


@app.post("/exports")
async def create_export(file: UploadFile) -> dict[str, str]:
    export_id = str(uuid4())

    # Persist the input in durable object storage instead of
    # the local filesystem of the application instance.
    object_key = f"exports/{export_id}/input"

    await object_storage.put(
        key=object_key,
        content=file.file,
    )

    # Queue expensive processing instead of keeping
    # the HTTP connection open.
    await job_queue.publish({
        "export_id": export_id,
        "object_key": object_key,
    })

    return {
        "export_id": export_id,
        "status": "accepted",
    }

The application instance can disappear immediately after this response without losing the uploaded file or queued work. That property is what makes aggressive horizontal scaling and rolling deployments practical.

For a deeper explanation of this architecture pattern, see: Scaling Stateless Applications.

Synchronous vs Asynchronous Work

Synchronous processing is appropriate when the caller requires the result before continuing. Authentication, inventory validation, and many transactional operations belong in this category.

Asynchronous processing is better when work is expensive, bursty, retryable, or not required to construct the immediate response. Email delivery, analytics, media processing, report generation, search indexing, and many integration workflows fit naturally behind queues.

Property Synchronous Asynchronous
Client Latency Includes processing time Usually short acknowledgement
Failure Handling Failure immediately affects request Can retry independently
Traffic Spikes Immediately hit downstream services Queue absorbs temporary bursts
Consistency Easier immediate consistency Often eventually consistent
Operational Complexity Lower initially Requires queues, workers, retries, and idempotency
Best For Immediate decisions Background and bursty workloads

Queues are not unlimited capacity. If producers consistently create work faster than consumers process it, queue depth becomes stored latency. Production systems should monitor queue depth, message age, consumer throughput, retry rates, and dead-letter volume.

Designing for Elasticity

Elasticity means increasing and decreasing capacity as demand changes. Horizontal scaling is usually straightforward for stateless compute, but scaling an application tier does not automatically scale its dependencies.

Suppose each application instance maintains 20 database connections:

10 instances  x 20 connections =   200 connections
50 instances  x 20 connections = 1,000 connections
200 instances x 20 connections = 4,000 connections

An autoscaling event intended to protect the API can therefore overload the database. Scaling one layer can move the bottleneck into another layer.

Production capacity planning should track the entire resource chain:

  • request rate and concurrent requests;
  • CPU and memory utilization;
  • application worker saturation;
  • database connections and query latency;
  • cache hit ratio and memory pressure;
  • queue depth and oldest-message age;
  • downstream API quotas;
  • network throughput and connection limits.

CPU utilization is useful for CPU-bound workloads but is often a poor scaling signal for I/O-heavy APIs. Request concurrency, request rate per instance, queue depth, or application-specific saturation metrics can provide earlier signals.

Scaling should also happen before capacity is exhausted. If a new container requires 90 seconds to become ready, scaling at 95% utilization may be too late.

autoscaling:
  min_replicas: 4
  max_replicas: 40

  metrics:
    cpu_target_percent: 60
    requests_per_instance: 250

  scale_out:
    cooldown_seconds: 30

  scale_in:
    # Scale in more conservatively to avoid oscillation.
    cooldown_seconds: 300

Scale-out and scale-in should rarely be equally aggressive. Adding capacity early protects latency. Removing capacity too quickly can create oscillation when traffic fluctuates around a threshold.

Cost is part of this trade-off. Maintaining spare capacity increases cost but improves response to sudden traffic. Running close to maximum utilization improves resource efficiency but reduces the margin available for failures, deployments, and bursts.

Designing for Failure

Cloud infrastructure does not eliminate failures. It changes their shape. Individual machines become disposable, but applications gain dependencies on networks, control planes, managed services, DNS, identity systems, quotas, and distributed data stores.

A robust architecture assumes that individual compute nodes will disappear. Multiple application replicas should run across independent failure domains, and traffic should stop reaching an instance before it is terminated or during an unhealthy deployment.

More critical systems may also replicate databases, caches, and other stateful components. Replication improves availability, but it introduces consistency and recovery trade-offs such as replica lag, stale reads, failover delays, and split-brain protection.

For systems that must survive failure of an entire location, see: Designing Highly Available Cloud Systems and Multi-Region Architecture and Disaster Recovery.

Failure Scenarios

Failure handling should be designed explicitly rather than discovered during an incident.

Failure Expected Behavior Recovery
Application node crashes Health checks remove it from traffic Scheduler or autoscaler replaces capacity
Network request times out Request stops within a bounded deadline Retry only safe operations with backoff and jitter
Database primary fails Writes temporarily fail or pause Promote a healthy replica and reconnect clients
Cache becomes unavailable Traffic falls back carefully to durable storage Restore cache while protecting the database from a traffic surge
Worker dies Unacknowledged work becomes available again Another worker processes the idempotent job
Queue backlog grows Processing latency increases Scale consumers or reduce producer rate
Deployment fails Healthy old capacity continues serving traffic Stop rollout and revert
Availability zone fails Remaining zones continue serving traffic Replace capacity outside the failed zone

Retries deserve particular attention. Retrying immediately against an overloaded dependency can multiply traffic exactly when that dependency has the least spare capacity. Retries should use bounded attempts, exponential backoff, jitter, and request deadlines.

import asyncio
import random
from collections.abc import Awaitable, Callable
from typing import TypeVar

T = TypeVar("T")


async def retry_with_backoff(
    operation: Callable[[], Awaitable[T]],
    attempts: int = 4,
    base_delay: float = 0.1,
) -> T:
    for attempt in range(attempts):
        try:
            return await operation()
        except TimeoutError:
            if attempt == attempts - 1:
                raise

            # Jitter prevents many instances from retrying together.
            delay = base_delay * (2 ** attempt)
            delay += random.uniform(0, delay * 0.25)

            await asyncio.sleep(delay)

    raise RuntimeError("unreachable")

Retries are only safe when the operation is idempotent or protected with an idempotency mechanism. Reliability patterns such as retries, timeouts, circuit breakers, and load shedding are covered in more depth here: Reliability Best Practices for Production Systems.

Production Design Example

Consider a cloud-hosted logistics platform processing shipment creation, label generation, tracking updates, and carrier integrations. Interactive API requests require low latency, while carrier communication is slower and more failure-prone.

                         +----------------+
Internet ----------------> CDN / Edge     |
                         +-------+--------+
                                 |
                                 v
                         +-------+--------+
                         | Load Balancer  |
                         +-------+--------+
                                 |
                  +--------------+--------------+
                  |                             |
                  v                             v
             API Instance                  API Instance
                  |                             |
                  +-------------+---------------+
                                |
                 +--------------+--------------+
                 |              |              |
                 v              v              v
              Cache         Database      Object Storage
                                |
                                |
                           Transaction
                                |
                                v
                              Queue
                                |
                     +----------+----------+
                     |                     |
                     v                     v
                Carrier Worker        Carrier Worker
                     |
                     v
               External Carriers

The API validates the shipment request and writes durable shipment state before acknowledging it. Slow carrier calls are placed behind the queue instead of becoming part of the request path.

Workers consume carrier jobs independently. If a carrier API becomes slow, only that asynchronous workflow accumulates backlog. The customer-facing API does not need to keep thousands of HTTP connections waiting for the carrier.

Each carrier request should use an idempotency key or equivalent deduplication mechanism. A worker can crash after the carrier accepts a shipment but before the queue acknowledgement completes, causing the message to be delivered again.

The database remains a potential scaling bottleneck. Application replicas can grow rapidly, but transaction throughput, lock contention, storage I/O, and connection limits still constrain the durable data layer. Cache and read replicas can reduce specific read workloads, but they do not make write capacity unlimited.

Important production metrics include:

  • API: request rate, p50/p95/p99 latency, error rate, active requests, and saturation.
  • Database: query latency, connection utilization, lock waits, transaction rate, CPU, and storage latency.
  • Cache: hit ratio, memory utilization, evictions, hot keys, and command latency.
  • Queue: depth, oldest-message age, processing rate, retry rate, and dead-letter volume.
  • Workers: active jobs, job duration, failures, external dependency latency, and throttling.

This architecture also creates independent scaling dimensions. API replicas scale with interactive traffic, while carrier workers scale with queue depth. The two workloads no longer compete for the same execution capacity.

Infrastructure Example

The following CloudFormation fragment illustrates the underlying principle using AWS resources. The architecture itself is provider-independent: multiple application instances run behind a traffic distributor rather than exposing individual machines directly.

Resources:
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      Subnets:
        - !Ref PublicSubnetA
        - !Ref PublicSubnetB

  ApplicationTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 8080
      Protocol: HTTP
      VpcId: !Ref VPC
      HealthCheckPath: /health/ready
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3

  ApplicationAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      MinSize: "4"
      DesiredCapacity: "4"
      MaxSize: "40"
      VPCZoneIdentifier:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetB
      TargetGroupARNs:
        - !Ref ApplicationTargetGroup
      LaunchTemplate:
        LaunchTemplateId: !Ref ApplicationLaunchTemplate
        Version: !GetAtt ApplicationLaunchTemplate.LatestVersionNumber

Two independent failure domains are used so a single-zone failure does not remove the entire compute fleet. Application instances remain private, while the load balancer provides the public entry point.

The readiness endpoint should verify whether an instance can safely receive traffic, but it should not blindly require every optional downstream dependency to be healthy. Otherwise, a minor dependency failure can cause every application instance to remove itself from service simultaneously.

Deployment automation should gradually replace instances while observing readiness, error rate, and latency. A failed release should stop before all known-good capacity disappears.

Common Mistakes

Cloud failures are frequently caused not by the absence of managed services but by incorrect assumptions about scaling and dependency behavior.

Mistake Production Impact Better Approach
Keeping durable state on application instances Scaling, replacement, and deployments can lose state or require sticky routing. Move durable state to databases, object storage, caches, or queues designed for shared access.
Scaling compute without checking database capacity New instances exhaust connections or increase query load until the database becomes the bottleneck. Model connection and query capacity across the maximum compute fleet.
Putting non-critical work in the synchronous path Slow integrations increase user latency and reduce API availability. Move retryable background work behind queues.
Using CPU as the only scaling metric I/O-bound applications can saturate while CPU remains relatively low. Scale using workload-specific saturation metrics such as concurrency, request rate, or queue depth.
Retrying every failure immediately Retry storms amplify outages and overload recovering dependencies. Use deadlines, bounded retries, exponential backoff, jitter, and circuit breaking.
Treating queues as unlimited buffers Backlogs grow until jobs become operationally useless or retention limits are reached. Alert on message age and consumer throughput, not only queue depth.
Making health checks depend on every downstream service One dependency outage can remove all otherwise functional application instances. Separate liveness, readiness, and dependency monitoring.
Running all replicas in one failure domain A localized infrastructure failure becomes a complete application outage. Distribute critical capacity across independent failure domains.
Ignoring downstream quotas during autoscaling Additional application instances generate more calls and trigger throttling elsewhere. Include dependency quotas and rate limits in capacity planning.
Optimizing only for resource utilization Running near maximum capacity leaves insufficient headroom for bursts, failures, and deployments. Maintain intentional capacity headroom based on scaling and recovery time.

Production Checklist

Before a cloud application is treated as production-ready, verify the architecture under both normal load and degraded conditions.

  • Externalize durable state: confirm that replacing an application instance cannot lose sessions, files, jobs, or business data.
  • Define capacity limits: document maximum compute replicas, database connections, worker concurrency, API quotas, and storage limits.
  • Measure tail latency: alert on p95 and p99 latency rather than relying only on averages.
  • Protect dependencies: configure request deadlines, bounded retries, backoff, jitter, and concurrency limits.
  • Monitor queue age: track the oldest message in addition to queue depth so processing delay is visible.
  • Test instance failure: terminate active compute instances and verify that traffic moves to healthy capacity automatically.
  • Test dependency failure: simulate unavailable databases, caches, queues, and external APIs and verify controlled degradation.
  • Validate scaling signals: confirm that scaling begins early enough for new capacity to become ready before saturation.
  • Reserve failure headroom: ensure remaining capacity can handle traffic when part of the fleet becomes unavailable.
  • Protect the database: set connection limits and monitor query latency, lock contention, connection utilization, and storage pressure.
  • Make jobs idempotent: ensure duplicate message delivery cannot duplicate payments, shipments, emails, or other side effects.
  • Exercise deployments: verify that unhealthy releases stop automatically while known-good capacity remains available.

Conclusion

Modern cloud architecture is built around replaceable compute, externalized state, horizontal scaling, asynchronous boundaries, automated recovery, and explicit failure handling. Cloud infrastructure makes these patterns easier to implement, but it does not remove the engineering work required to design them correctly.

The most scalable design is not necessarily the one containing the most managed services or microservices. Every additional network dependency adds latency, cost, operational complexity, and another failure mode. Architecture should remain as simple as possible while meeting concrete availability, scalability, performance, and recovery requirements.

Key Takeaway: Design cloud systems around failure boundaries and state ownership first. Once compute can be replaced safely and workloads can scale independently, elasticity, high availability, deployment automation, and disaster recovery become significantly easier to implement.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)