A Highly Available Three-Tier Application on AWS: Multi-Region Design with Aurora DSQL

View Markdown

Listen to this article
AI-narrated listening version - 10 min 06 sec. Code is explained; complete examples remain below.

Read the narration transcript - Open audio

Designing a three-tier AWS application that survives an Availability Zone or Region failure. Explore the architecture diagram, service choices, database consistency, identity recovery, and the trade-offs behind each decision.

TL;DR

A highly available three-tier AWS application can combine CloudFront and private S3 buckets for presentation, Global Accelerator and ECS Fargate for regional application services, and Aurora DSQL for strongly consistent writes across two Regions. Each application Region spans three Availability Zones and maintains failover capacity. A third Region acts as the DSQL witness, rather than another application deployment. Cognito multi-Region replication improves identity recovery but has feature limitations. This design explains regional routing, transaction retries, deployment safety, backup recovery, and the distinction between database availability and end-to-end application reliability.

Start with the user journey, then choose the services

A three-tier AWS application separates presentation, business logic, and persistent data. To keep that application useful during a Region failure, every dependency in the user journey needs a recovery strategy: serving the page, signing in, executing a request, and finding the committed result afterward.

Consider a greenfield order-management application. Users browse orders, submit new ones, and check their status. The front end is a static single-page application; the API runs in containers; the database needs SQL transactions and consistent reads across Regions. Payment processing and large file uploads are separate integrations with their own recovery contracts.

The proposed architecture uses two application Regions: Ireland (eu-west-1) and London (eu-west-2). Paris (eu-west-3) is the Aurora DSQL witness. This is a supported European DSQL Region combination. London is in the UK: this placement is not an EU-only data-residency design. Confirm jurisdiction requirements for application data, identity data, logs, and witness transaction logs before choosing locations. DSQL Region availability

This reference design targets continued order access for already-authorized users and existing users whose authentication method supports identity failover. It does not promise that every account-administration operation survives every failure.

Service behavior was checked against AWS documentation on 4 October 2026. “Latest” here means relevant, generally available capabilities, including DSQL's newer relational features and Cognito multi-Region replication. It does not mean adding every recently launched service. Start by checking the DSQL compatibility guide against your application before committing to this design.

The AWS architecture diagram

AWS multi-region three-tier architecture: CloudFront serves two private S3 origins; Global Accelerator routes APIs to internal ALBs and ECS Fargate across three Availability Zones in Ireland and London; paired Aurora DSQL endpoints use a Paris witness; Cognito has a primary and standby replica.
AWS multi-region three-tier architecture. Select the diagram to view it at full size.

Solid arrows show request paths. Dashed connections show replication or coordination. The witness does not serve application requests. Aurora DSQL sits outside the customer VPC; PrivateLink provides the private connection into the managed service. Identity is a separate dependency with its own failover path.

View the full-size architecture diagram.

The three logical tiers remain distinct even though they use several managed services:

TierComponentsAvailability strategy
PresentationRoute 53, CloudFront, WAF, two private S3 bucketsEdge caching and S3 origin failover; deploy the same assets to both Regions
ApplicationGlobal Accelerator, regional internal ALBs, regional WAF, ECS FargateBoth Regions serve traffic; each spans three AZs and retains failover capacity
DataTwo peered Aurora DSQL clusters and a third-Region witnessStrongly consistent regional read/write endpoints; managed recovery

Cognito, IAM, KMS, ECR, CloudWatch, and AWS Backup support these tiers. They are not additional business tiers.

Tier 1: deliver the front end without depending on the API Region

Point app.example.com through a Route 53 alias to CloudFront. Use S3 REST origins, keep both buckets private, and configure Origin Access Control for each origin. Block public access. The bucket policies permit the intended CloudFront distribution rather than anonymous website access. OAC is incompatible with S3 website endpoints, which have different security behavior. CloudFront origin access

Configure an origin group with Ireland primary and London secondary. Select appropriate connection-failure and server-error conditions, and tune timeout behavior through tests. CloudFront origin failover handles GET, HEAD, and OPTIONS; it does not provide generic failover for POST or PUT. That makes it suitable for immutable JavaScript, CSS, and HTML delivery, while the API uses its own routing path. CloudFront origin failover

The deployment pipeline copies the same content-hashed assets to both buckets before updating either Region's entry HTML. For example, in a CI job with regional deployment permissions:

# dist/assets contains content-hashed filenames. Retain older release assets.
aws s3 sync dist/assets/ "s3://${WEB_BUCKET_IRELAND}/assets/" \
  --region eu-west-1 --cache-control 'public,max-age=31536000,immutable'
aws s3 sync dist/assets/ "s3://${WEB_BUCKET_LONDON}/assets/" \
  --region eu-west-2 --cache-control 'public,max-age=31536000,immutable'

# Run after verifying both Regions contain every asset referenced by index.html.
aws s3 cp dist/index.html "s3://${WEB_BUCKET_IRELAND}/index.html" \
  --region eu-west-1 --cache-control 'no-cache'
aws s3 cp dist/index.html "s3://${WEB_BUCKET_LONDON}/index.html" \
  --region eu-west-2 --cache-control 'no-cache'

These commands are a release fragment, not a complete deployment pipeline. Add manifest verification, rollback, security headers, and an SPA route-rewrite strategy. Deploying assets directly into both Regions avoids making a new release's availability depend on asynchronous bucket replication catching up.

Why these components? S3 removes web-server maintenance for static content. CloudFront keeps repeated reads near users and reduces origin load. Route 53 supplies durable names without becoming the per-request application router. WAF filters malicious HTTP requests at the presentation boundary. Server-side rendering would require an additional resilient rendering service; it is outside this static-front-end example.

Tier 2: route API requests to two independent application deployments

Point api.example.com through Route 53 to a standard AWS Global Accelerator. Its regional endpoint groups contain the Ireland and London ALBs. Both serve traffic; Global Accelerator selects endpoints using health, client location, and configured routing controls. Traffic dials are controls for regional participation, not a promise of an exact global 50/50 split. New connections can move after failure detection; existing connections and interrupted requests are not replayed. Global Accelerator routing

Use an internal ALB in each VPC, with subnets across three AZs. Global Accelerator can reach internal ALBs without giving them public IPs. Its VPC still needs an attached internet gateway, although the private ALB subnets do not need an internet-gateway route. Attach a regional WAF web ACL to each ALB; Global Accelerator itself is not an HTTP firewall or cache. Private Global Accelerator endpoints

Configure HTTPS listeners, regional ACM certificates, IP target groups, security groups, and a bounded readiness endpoint. Keep Fargate tasks in private subnets with no public IP assignment. Permit application ingress only from the ALB security group. Separate database access from application ingress.

ECS Fargate suits this workload because it runs ordinary containers without EC2 host patching or a Kubernetes control plane. EKS is reasonable when Kubernetes APIs, operators, or platform standardization justify its additional operations. Lambda is another option for short event-driven requests, but this example deliberately keeps a container service with controlled connection pooling and request concurrency.

Enable ECS Availability Zone rebalancing explicitly and maintain tasks in all three AZs. It is a recovery mechanism, not a substitute for spare capacity. A service fragment using rolling deployments might look like this:

# Fragment: referenced VPC, task definition, listener and target group already exist.
ApiService:
  Type: AWS::ECS::Service
  DependsOn: ApiHttpsListener
  Properties:
    Cluster: !Ref ApiCluster
    TaskDefinition: !Ref ApiTaskDefinition
    LaunchType: FARGATE
    DesiredCount: 9 # Illustrative; derive your minimum from a load test.
    AvailabilityZoneRebalancing: ENABLED
    DeploymentConfiguration:
      MinimumHealthyPercent: 100
      MaximumPercent: 200
      DeploymentCircuitBreaker:
        Enable: true
        Rollback: true
    NetworkConfiguration:
      AwsvpcConfiguration:
        AssignPublicIp: DISABLED
        SecurityGroups: [!Ref ApiTaskSecurityGroup]
        Subnets: [!Ref PrivateSubnetA, !Ref PrivateSubnetB, !Ref PrivateSubnetC]
    LoadBalancers:
      - ContainerName: api
        ContainerPort: 8080
        TargetGroupArn: !Ref ApiTargetGroup

Create this regional service twice through infrastructure as code. Add target-tracking scaling, request timeouts, graceful shutdown, connection draining, and quotas for surge tasks. The fragment follows current ECS CloudFormation properties and AZ rebalancing behavior. Validate the finished stack rather than treating the fragment as a deployable template.

The browser calls a separate API hostname, so allow only the intended front-end origin through CORS. Validate access-token signature, issuer, expiration, client binding and required scopes in both Regions. Store durable state in the database, not task memory. Removing a task must not remove a session, order, or idempotency record.

Reserve capacity before the outage

Assume a hypothetical global peak of 400 requests/second. Suppose a representative load test establishes 100 requests/second per task, and you choose a safe operating budget of 70 per task. To handle the entire global peak after losing one of three evenly populated AZs:

Required tasks per Region >= ceil(global_peak / (safe_task_rate × 2/3))
                         >= ceil(400 / (70 × 2/3))
                         >= 9

After one AZ is lost: 6 tasks × 70 requests/second = 420 requests/second.

These numbers are a sizing example, not an AWS benchmark. Validate the same scenario against database contention, connection rates, downstream limits, memory, and tail latency. Autoscaling cannot manufacture instantaneous capacity during a regional evacuation.

Tier 3: choose Aurora DSQL for the required write semantics

Aurora DSQL gives this greenfield application two regional endpoints that accept reads and writes against strongly consistent logical data. DSQL manages its infrastructure and AZ resilience. The Paris witness stores a limited window of encrypted transaction logs for coordination and durability; it has no application endpoint and is not a third full database copy. Pair the two clusters using the documented multi-Region configuration. DSQL resilience

Why DSQL? The requirement is to accept orders from either healthy application Region without promoting a new database writer. This makes active-active database behavior useful. Cross-Region coordination still costs latency: a nearby endpoint does not guarantee local-only commit latency. Benchmark the actual transaction shape across the selected geography.

AWS documents 99.999% multi-Region DSQL availability. That database property is not a five-nines promise for this whole application. Authentication, request routing, capacity, bad releases, client behavior, and external systems also influence the user journey. DSQL overview

PostgreSQL compatibility is a design gate

DSQL uses PostgreSQL-compatible interfaces, but it is not interchangeable with every PostgreSQL deployment. Current limits include one built-in postgres database, fixed Repeatable Read isolation, and restrictions on temporary tables, triggers, and procedural PL/pgSQL functionality. DDL and DML require separate transactions. Migration considerations

Conversely, older advice that DSQL lacks foreign keys is outdated: AWS added foreign-key constraints in August 2026. Check current syntax and workload costs rather than repeating preview-era restrictions. Foreign-key release

A minimal order record can use the client's operation identifier as its primary key:

-- Apply schema changes separately from application transactions.
CREATE TABLE orders (
    request_id UUID PRIMARY KEY,
    customer_id UUID NOT NULL,
    payload_hash VARCHAR(64) NOT NULL,
    total_cents BIGINT NOT NULL,
    status VARCHAR(32) NOT NULL
);

Validate amounts and authorization in the application. If the same operation identifier appears again, read the committed record, check ownership and payload hash, and return its original result. Reject reuse with a different payload. A client timeout does not prove that its transaction failed.

Connect privately and authenticate with the task role

Provision the DSQL connection interface endpoint in multiple AZs, enable private DNS, and restrict its security group to the task security group on port 5432. The management API endpoint alone does not provide SQL connectivity. Discover the connection service identifier using DSQL's get-vpc-endpoint-service-name operation and use its documented private hostname. DSQL PrivateLink

For a container with a least-privilege ECS task role, a connection factory can use IAM authentication:

python -m pip install boto3 'psycopg[binary]'
import os
import boto3
import psycopg

region = os.environ["AWS_REGION"]
cluster_id = os.environ["DSQL_CLUSTER_ID"]
service_id = os.environ["DSQL_CONNECTION_SERVICE_ID"]
host = f"{cluster_id}.{service_id}.{region}.on.aws"
dsql = boto3.client("dsql", region_name=region)

def open_connection():
    token = dsql.generate_db_connect_auth_token(
        Hostname=host, Region=region, ExpiresIn=900
    )
    return psycopg.connect(
        host=host, port=5432, dbname="postgres",
        user=os.environ["DSQL_DATABASE_ROLE"], password=token,
        sslmode="verify-full",
        sslrootcert=os.environ["DSQL_CA_BUNDLE"],
        connect_timeout=5,
    )

The CA bundle must exist in the image and trust the server certificate. The role needs dsql:DbConnect and the corresponding database-role authorization; reserve admin privileges for provisioning. Use this factory when establishing pooled connections, not on every HTTP request. In the private-DNS configuration, sign and connect to the same documented hostname. Token generation

Tokens authenticate new connections; expiry does not terminate existing sessions. DSQL nevertheless has connection-lifetime and creation-rate limits. Recycle pools with jitter before the documented 60-minute connection limit, and avoid synchronized reconnection storms against the 100-new-connections-per-second limit. Keep transactions within the current five-minute, 3,000-mutated-row and 10 MiB change limits. Authentication, DSQL quotas

Retry the transaction, preserve the business operation

DSQL uses optimistic concurrency control. A conflicting transaction can fail at commit with SQLSTATE 40001; retry the whole transaction with a bounded budget and jitter. Strongly consistent reads do not make snapshot-isolated transactions serializable: explicitly protect business invariants, such as inventory limits, and test concurrent updates. This example assumes a configured psycopg connection pool:

import random
import time
from psycopg.errors import SerializationFailure

def run_transaction(pool, work):
    for attempt in range(5):
        try:
            with pool.connection() as connection:
                with connection.transaction():
                    return work(connection)
        except SerializationFailure:
            if attempt == 4:
                raise
            time.sleep(random.uniform(0, min(0.05 * 2**attempt, 0.5)))

work must contain database operations only. It must reuse the same business operation identifier across retries. Add an overall request deadline and cancellation handling in the real API. Do not retry all exceptions indiscriminately, and do not charge a payment card inside this block. Network errors around commit require reconciling the original operation before attempting it again. DSQL concurrency control

For asynchronous work, write an outbox row in the same transaction, then let workers deliver it with deduplication. Regional queues, event buses, and payment providers have separate failure semantics; adding SQS does not create a globally replicated queue or exactly-once external side effects.

Identity also needs regional recovery

Cognito multi-Region replication, launched in June 2026, provides a primary user pool and a standby replica. It requires an eligible pool on Essentials or Plus and a KMS multi-Region customer-managed key. This design puts the primary in Ireland and its standby in London. Directory replication is eventually consistent; user administration is not active-active. Cognito release

Configure and activate the replica, its regional dependencies, and managed-login domain failover. The domain's routing configuration specifies the secondary Region and a Route 53 health check. Applications using regional SDK authentication endpoints must select the healthy endpoint themselves. The API's Global Accelerator does not automatically fail over Cognito. Cognito domain routing

Use Cognito's updated issuer configuration before launching this greenfield app. Both API deployments validate the same issuer and appropriate keys using an OIDC/JWT library. Updated issuers currently do not work with ALB's native authenticate-cognito action or API Gateway's native Cognito authorizers; therefore the ECS application performs token validation. Cache discovery and signing keys safely and handle rotation. Issuer compatibility

During identity failover, disable unsupported account operations in the UI. Secondary pools cannot register new users, reset passwords, or update profiles. TOTP MFA is unsupported there, and previously unseen federated users cannot sign in. Recently changed identity data may also lag. If those journeys must remain available during a Region outage, this identity choice needs revision before approving the architecture. Existing valid tokens can support authorized API requests independently of a new login, subject to token validity and policy. Cognito replication limitations

Why each supporting component was chosen

ComponentRationaleTrade-off or operating obligation
IAM task roles and SQL rolesShort-lived credentials and explicit application privilegesTest least-privilege grants in both Regions; distinguish task and execution roles
AWS KMSManaged encryption keys; multi-Region CMK where Cognito requires itRegional key policies and grants still need configuration; a key is not a replicated application
ECR regional repositoriesEach Region can start tasks from a local artifactConfigure replication and verify the exact image digest exists before release
VPC endpointsPrivate access to supported dependencies without mandatory public internet traversalEndpoint ENIs, security groups, DNS, and costs need regional design
Regional NAT Gateway, when neededManaged multi-AZ outbound internet access without maintaining separate zonal NAT resourcesVerify expansion into every workload AZ before relying on it; multi-AZ billing still applies
CloudWatch and distributed tracingObserve actual orders, errors, saturation, and dependency latencyTelemetry must work in both Regions; dashboards do not fix failures
AWS BackupRecover from deletion or corruption that live replication would reproduceFull recovery points, restore time, new cluster endpoints, and restore drills
Infrastructure as codeRepeat regional networks, services, policies and configurationValidate service differences and quotas; avoid making a failover require a new deployment

ECR replication helps distribute artifacts, but it is not a release-readiness guarantee. Promote an immutable digest only after checking both regional repositories. ECR replication

For private tasks, plan endpoints for ECR APIs and image pulls, S3 artifact access, CloudWatch Logs, and any chosen secret service. Fargate image pulls need the relevant ECR endpoints and S3 access, not just the ECR API endpoint. Private artifact access

If the API calls an internet-hosted payment or identity service, use Regional NAT Gateway in each application VPC. This newer availability mode expands across AZs based on workload presence, simplifying egress management. Expansion into a new AZ can take up to 60 minutes: establish and verify coverage across all three AZs before accepting production traffic. If you choose conventional zonal NAT gateways instead, use one per workload AZ with local routes. A single shared zonal gateway would introduce an avoidable AZ dependency. Test egress failure and include NAT's multi-AZ costs in the budget. Regional NAT behavior

Failure behavior: what happens when something breaks?

FailureExpected responseWhat to verify
One Fargate taskALB removes the unhealthy target; ECS replaces itDraining, retry behavior, idempotency and replacement capacity
One application AZRemaining targets serve traffic; ECS rebalances when capacity permitsReserved two-AZ capacity, endpoints and egress in surviving AZs
One application RegionGlobal Accelerator steers new connections to healthy endpointsHealth detection, full regional capacity, interrupted requests and client backoff
One DSQL peer RegionSurviving endpoint continues according to managed DSQL recovery behaviorCommit outcomes, contention, pool reconnect and regional app dependencies
Witness Region aloneDSQL documents continued cluster availabilityDo not mistake the witness for a third serving database
Primary S3 originCloudFront can use the secondary for eligible requestsBoth copies of all referenced assets, correct origin permissions and timeouts
Primary Cognito RegionSupported authentication shifts through configured identity failoverExisting users, issuer/JWKS access, method limits and degraded account operations
Bad release or accidental deleteStop rollout; roll back code or restore an appropriate backupReplication also propagates bad data; restore is a separate recovery workflow

An important routing caveat: Global Accelerator fails open when no healthy eligible endpoints remain. Health checks are not a security boundary or a guarantee that invalid requests stop arriving. Return explicit application errors and preserve authorization even during failures. Avoid coupling both Regions' readiness to one briefly unhealthy shared dependency. Unhealthy endpoint routing

A successful process-health probe only proves a process answered. Use bounded regional readiness and independent synthetic transactions to measure whether users can actually authenticate, create an order, and read it. Run canaries against each Region as well as the public hostname; otherwise global routing can conceal an unhealthy regional deployment.

Backups, recovery objectives, and safe deployment

Define an initial 99.95% successful-order SLO as a business target, not an observed result. Over a 30-day window that permits 21.6 minutes of unavailable time. Define separate latency and authentication SLOs and specify excluded journeys explicitly.

For one Region's infrastructure loss, design for preserving committed DSQL transactions. An acknowledged database commit and an HTTP success response are different boundaries; reconcile ambiguous outcomes with the operation identifier. Measure recovery time from user-visible interruption through routing, retries, identity recovery and successful completion. Do not infer an end-to-end RTO from database availability alone.

For accidental deletion, choose a distinct backup RPO: for example, a proposed hourly recovery-point policy has different loss exposure from live regional resilience. AWS Backup currently takes full DSQL backups. A paired-cluster restore uses the original recovery point and an identical copied recovery point in a supported second Region, creating new clusters. Reconfigure endpoints and validate permissions and connectivity before switching traffic. Do not describe this as DSQL point-in-time recovery. Backup behavior, Paired restore

Deploy one application Region at a time. Verify the new revision under synthetic and real traffic before rolling out the other. Use backward-compatible schema changes because both Regions share database state; a regional code rollback cannot undo a destructive shared migration. Confirm local artifacts, permissions, certificates and quotas before permitting traffic. Track transaction-conflict rates and tail latency alongside HTTP errors.

Before production approval, run these drills:

  1. Drain one AZ and load the remaining two at the intended failover rate.
  2. Withdraw one API Region and measure complete user journeys, including retries.
  3. Submit the same operation through both Regions and verify one committed outcome.
  4. Interrupt a connection near commit and prove reconciliation prevents duplicate work.
  5. Exercise Cognito failover for every authentication method you actually offer.
  6. Restore a backup into isolated resources and validate data and application behavior.
  7. Roll back one Region while the other runs the newer schema-compatible revision.

For related operational depth, see the DevOpsDreams guides on Terraform module design, Terraform state and backups, and SRE error budgets. Use the AWS restore procedure as an exercised runbook rather than a document left until an incident.

When a different database or routing choice makes more sense

Choose Aurora PostgreSQL Global Database when an existing application's SQL features, extensions or operational tooling outweigh active-active writes. It has a primary writer and asynchronous cross-Region replication. Unplanned failover can lose changes corresponding to replication lag; planned switchover can synchronize first. That is a different recovery contract from this DSQL design. Global Database recovery

Consider DynamoDB when access patterns naturally fit key-based operations and its selected multi-Region consistency mode meets the business contract. Do not migrate relational joins and transaction logic just to add another managed service. Compare databases against access patterns, consistency, compatibility, geography and cost.

Route 53 health-aware DNS routing can replace Global Accelerator for a simpler API entry point when resolver caching and client failover behavior are acceptable. This design pays for Global Accelerator to obtain an independent global network entry point and regional routing controls. It keeps CloudFront for cacheable content, where its behavior fits.

Cost review should include failover headroom, Fargate tasks, two ALBs, Global Accelerator, WAF, data transfer, DSQL usage, Cognito replication, endpoint ENIs, NAT where needed, observability retention and backup copies. Regional resilience increases the minimum operating footprint. If a slower recovery is acceptable, warm standby may fit the business better than running both application Regions at full recovery capacity.

Frequently asked questions

What are the three tiers in a highly available AWS application?

The presentation tier serves the interface, the application tier executes business logic, and the data tier stores authoritative state. Here they use CloudFront and S3, ECS Fargate behind ALBs, and Aurora DSQL. Cross-tier dependencies still need explicit recovery behavior.

Can both Regions write to the database at the same time?

Yes. The paired Aurora DSQL endpoints support strongly consistent reads and writes against shared logical data. That does not eliminate transaction conflicts, cross-Region latency, or the need to reconcile unknown commit outcomes.

Does this architecture guarantee zero downtime?

No. It removes several regional failure dependencies but cannot eliminate health-detection delays, interrupted requests, bad deployments or every identity limitation. Establish SLOs and measure complete user journeys in failure drills.

When should I choose Aurora Global Database instead?

Choose it when conventional PostgreSQL or MySQL compatibility and migration requirements matter more than independent regional write endpoints. Design for its primary writer, replication lag, and explicit regional failover rather than assuming DSQL semantics.

Will every Cognito operation work during regional failover?

No. Secondary authentication has limits, including unavailable sign-up, password reset and profile updates, and unsupported TOTP authentication. Verify the current feature matrix and revise the identity design if those journeys require the same availability target as order access.

Comments

Popular posts from this blog

Argo CD Auto-Sync and Health Checks: An Operator's Guide to Safe GitOps Reconciliation

Applying SRE Error Budgets to Services Running on EKS: SLIs, Burn Rates, and Release Decisions

Progressive Delivery with Canary and Blue-Green on Kubernetes: Argo Rollouts, Flagger, and Gateway API