A sound first AWS architecture begins with the application’s needs, not a catalog of popular services. This guide develops a simple task-management web app step by step and shows how its frontend, API, database, file storage, networking, and security controls work together.

1. Define the Web Application Before Choosing AWS Services

Before choosing AWS services, record what the first release must actually do. This avoids a common beginner error: selecting a service because it seems useful and then forcing the application to fit it.

For this example, imagine a task manager where users can create accounts, add tasks, mark them complete, and optionally attach files.

Start With the Request Path

Break the application into the pieces the user interacts with:

Frontend

  • A browser loads HTML, CSS, JavaScript, and images.
  • The frontend displays tasks and sends requests to an API.

Backend API

  • The API creates, reads, updates, and deletes tasks.
  • It verifies which user made each request.
  • It may generate secure upload and download links for file attachments.

Persistent data

  • User profiles and task records must still be available after a request ends.
  • Tasks may have relationships, such as projects containing many tasks.

File storage

  • Attachments, profile images, exports, and generated reports belong outside the database.

Authentication

  • Users need a way to sign in.
  • The application must identify the user behind every API request.

Operational needs

  • The team needs logs when something fails.
  • The architecture should support both development and production environments.
  • Spending needs basic guardrails before an abandoned test resource quietly starts collecting rent.

Separate the First Release From Later Possibilities

List first-version requirements separately from ideas for later. The initial release may need only a web frontend, an API, a relational database, and authentication.

Future requirements might include:

  • Live updates when a task changes
  • Background processing for large imports
  • Search across many records
  • Global delivery for users in multiple regions
  • Data analytics pipelines
  • Mobile push notifications

Do not build all of those on day one. A useful architecture solves the known problem now and remains changeable when evidence points in a different direction.

A Practical First Architecture Target

For the task manager, the initial requirements might be:

  • A single-page web frontend
  • User sign-up and sign-in
  • A REST-style API
  • SQL queries for users, projects, tasks, and comments
  • Optional attachments stored as files
  • Moderate and unpredictable early traffic
  • No need for a server running constantly

Those requirements suggest a small, managed architecture rather than a fleet of servers.

Real story

I once tried to “keep it simple” by wiring up a tiny demo app with three AWS services, a VPC, and a security group plan I drew on a napkin. By the time I was done, my browser tab had more console errors than code. I finally deployed the whole thing and celebrated, only to realize the frontend was pointing at the wrong API endpoint and my task app was proudly saving every todo into digital nowhere.

Have a story of your own? Share it in the comments below.

2. Choose How the Application Runs: Static Hosting, Containers, or Managed Compute

The compute choice affects request handling, deployment, and scaling. Begin with how the application behaves at runtime, rather than with a service name.

For the task manager, a static frontend paired with a request-driven API is a sensible starting point.

Host the Frontend as Static Files

When the frontend is a React, Vue, Angular, or similar single-page application, its production output is generally a collection of static files. Amazon S3 object storage can store those files, while Amazon CloudFront delivers them to users.

A common arrangement is:

  • Amazon S3 stores the built frontend files.
  • Amazon CloudFront caches and serves those files through HTTPS.
  • Amazon Route 53 maps a custom domain, such as app.example.com, to CloudFront.

Keep the S3 frontend bucket private. For a current private-origin setup, configure a CloudFront Origin Access Control (OAC) for the S3 origin and add an S3 bucket policy that permits that CloudFront distribution to read the frontend objects.

Use the standard S3 bucket origin with CloudFront, not the S3 static website endpoint. The website endpoint does not support the CloudFront OAC private-origin pattern. Configure single-page application routing through CloudFront and the S3 origin behavior as needed, while keeping public S3 access blocked.

This arrangement fits applications where the browser handles rendering and the frontend does not need a server to generate HTML for each request.

Use AWS Lambda and Amazon API Gateway for a Small API

For an API that runs only when requests arrive, AWS Lambda and Amazon API Gateway are often a practical starting point.

In this model:

  • Amazon API Gateway receives HTTPS API requests.
  • AWS Lambda runs the application code for each request.
  • The Lambda function reads or writes data, then returns a response.
  • AWS scales request handling without requiring a server process to remain running.

For example, an API request to create a task could look like this:

POST /tasks
Authorization: Bearer 

API Gateway passes the request to a Lambda function. The function validates the input, identifies the signed-in user, inserts the task into the database, and returns the new record.

This approach is usually a good fit when:

  • Requests are short-lived.
  • Traffic is uneven or still unknown.
  • The team prefers less infrastructure management.
  • The backend can be divided into small API functions or a modest service layer.

Lambda also has trade-offs. A function may have a startup delay after sitting idle, particularly with certain runtimes and configurations. Long-running jobs, persistent connections, and complex background workers may fit containers better.

Plan Lambda-to-RDS Connections Before Traffic Bursts

Each concurrent Lambda execution can open database connections. During a burst, those direct connections can multiply quickly and exhaust the connection limit on an RDS instance.

For Lambda functions that access RDS, use this decision rule:

  • Use Amazon RDS Proxy when Lambda traffic can burst, when multiple functions share the database, or when expected concurrency is difficult to predict. RDS Proxy manages a pool of database connections and reduces the number of direct connections reaching RDS.
  • A direct Lambda-to-RDS connection can be acceptable for a low-concurrency workload when maximum concurrency is known and deliberately limited. Reuse connections where the runtime allows it, use a small application-side connection pool, and set Lambda reserved concurrency below the database connection capacity, with room for administrative and other application connections.

RDS Proxy does not eliminate the need to monitor slow queries, connection counts, and Lambda concurrency. It manages connections; it does not replace database design or operational limits.

Choose Amazon ECS With AWS Fargate When the App Needs Containers

Amazon ECS with AWS Fargate runs containerized applications without requiring you to manage the underlying servers. It is useful when the backend needs more runtime control than Lambda provides.

Consider this path when:

  • The application already runs in Docker containers.
  • Requests may take longer to process.
  • You need a process that stays running.
  • You use a framework that is easier to operate as a conventional web server.
  • You need custom dependencies or more predictable startup behavior.

A typical ECS design places an Application Load Balancer in front of one or more Fargate tasks. The load balancer sends incoming HTTPS requests to healthy containers.

This option gives you more runtime flexibility, but it also introduces decisions about container images, task sizing, scaling rules, load balancing, and network configuration. None of these is inherently difficult, but they create more moving parts than a basic Lambda API.

Consider AWS Elastic Beanstalk When You Want a Guided Platform

AWS Elastic Beanstalk can help teams with a traditional web application that want AWS to assemble several infrastructure components around it. You deploy application code, select a supported platform, and Elastic Beanstalk manages much of the environment setup.

It can be a reasonable middle path for a beginner deploying a conventional application server. Even so, learn what it creates and manages on your behalf. A managed platform reduces setup work, but it does not remove the need to understand deployments, logs, permissions, networking, and database access.

Select a Compute Path Based on the Application’s Behavior

For the task manager’s first release, a practical choice is:

  • S3 and CloudFront for the browser frontend
  • API Gateway and Lambda for the API
  • RDS for relational task data
  • S3 for attachments

This choice fits the application because it has a static frontend, short API requests, and no known need for permanently running processes.

Move toward ECS with Fargate later if actual requirements emerge, such as long-running work, WebSocket-heavy services, or a backend that is easier to package and operate in containers.

3. Add Networking and Data Services Around the Compute Layer

After selecting compute, decide how users will reach it and where the data will live. The aim is not the largest possible network diagram. It is a clear request path that keeps sensitive components off the public internet.

Map the Request Flow

A simple version of the task manager architecture could look like this:

User browser
    |
    v
Amazon Route 53
    |
    v
Amazon CloudFront
    |-----------------------------> Private Amazon S3 bucket
    |                                 Static frontend files
    |
    v
Amazon API Gateway
    |
    v
AWS Lambda
    |
    +-----------------------------> Amazon RDS database
    |                                 Private subnets
    |
    +-----------------------------> Amazon S3 bucket
                                      User attachments

Amazon Cognito
    |
    v
Signs in users and provides tokens for API requests

The user first loads the frontend through CloudFront. The browser then calls the API. API Gateway invokes Lambda, which accesses the database or creates a limited upload or download URL for S3.

You can send both frontend and API traffic through CloudFront, or use separate subdomains such as app.example.com and api.example.com. Either design can work. Choose a route that is easy to explain and document.

Configure Browser Cross-Origin Requests Deliberately

When the frontend and API use different origins, the browser requires cross-origin configuration. For example, app.example.com calling api.example.com requires API Gateway CORS settings that allow the frontend origin.

You have two main options:

  • Route API requests through a CloudFront behavior such as /api/* so the browser sees the frontend and API as the same origin.
  • If the frontend and API use separate origins, configure API Gateway CORS narrowly for the frontend origin.

For an API Gateway CORS configuration, allow only the expected frontend origin, required request methods, and required request headers, such as Authorization and Content-Type when the application uses them. Do not use an unrestricted origin simply to make browser testing easier.

Direct browser uploads to S3 also require an S3 CORS rule. Limit that rule to:

  • The frontend origin
  • The upload methods the application actually uses, such as PUT
  • The headers required by the presigned upload request, such as Content-Type and any signed headers

Keep the attachment bucket private. S3 CORS controls which browser origins may make requests; it does not make objects public or replace IAM and presigned URL permissions.

Use Route 53, CloudFront, and Load Balancers for Their Specific Roles

These services perform different jobs:

  • Amazon Route 53 provides DNS. It maps a domain name to the service that should receive traffic.
  • Amazon CloudFront caches and delivers web content close to users. It is especially useful for static frontend assets.
  • Amazon API Gateway accepts and manages API requests for a Lambda-based design.
  • Application Load Balancer routes HTTP and HTTPS traffic to container tasks or other application targets, making it more common in an ECS architecture.
  • Amazon VPC provides the network boundary where you place private application resources, such as databases and container workloads.

You do not need an Application Load Balancer in front of a Lambda API that already uses API Gateway. Without a clear reason, adding one creates another configuration surface to revisit later, often at the least convenient moment.

Choose a Database Based on How the Application Queries Data

For the task manager, Amazon RDS is often the straightforward choice because task data is relational.

A relational model may include tables such as:

  • users
  • projects
  • tasks
  • comments
  • task_attachments

Tasks belong to users or projects. Comments belong to tasks. Reports may need joins, filters, ordering, and transactions. These are familiar SQL database patterns.

Use Amazon RDS when the application needs:

  • SQL queries and joins
  • Clear relationships between records
  • Transactions that update related data reliably
  • Familiar relational database tools and concepts

Amazon DynamoDB follows a different model. It works well when access patterns are known in advance and data can be retrieved efficiently through partition keys, sort keys, and indexes.

Consider Amazon DynamoDB when:

  • Requests follow predictable key-based access patterns.
  • The data model fits key-value or document storage.
  • Very high scale or low-latency key lookups are a core requirement.
  • You are comfortable designing around DynamoDB keys and indexes.

For a first task manager, selecting RDS because the data is relational is not a failure to “go serverless.” It means the data model matches the application.

Store Files in S3, Not in Database Rows

Use Amazon S3 for large objects such as:

  • User attachments
  • Images
  • CSV exports
  • Generated reports
  • Backup files

Do not store file contents directly in database tables unless there is a specific reason. Databases are better suited to structured records and metadata.

For example, the database can store:

task_id: 842
file_name: project-plan.pdf
s3_key: attachments/user-174/task-842/project-plan.pdf
uploaded_at: 2026-09-10T14:30:00Z

The actual PDF lives in S3.

For uploads, a secure pattern is:

  1. The signed-in user asks the API to upload an attachment.
  2. The API verifies that the user can modify the task.
  3. The API creates a short-lived presigned S3 upload URL for a specific object key.
  4. The browser uploads the file directly to S3.
  5. The API records the attachment metadata in the database.

This keeps large files out of the Lambda function and limits upload permissions.

Private attachments also need an authorized download path:

  1. A signed-in user requests a specific attachment from the API.
  2. The API verifies that the user is allowed to read the related task.
  3. The API looks up the attachment’s S3 key from the database.
  4. The API returns a short-lived presigned GetObject URL for that specific private object.
  5. The browser uses that URL to download or display the attachment.

Do not expose a permanent public S3 object URL merely because a user is allowed to view a task today. A short-lived download URL limits access to the authorized request and lets the API apply task-level authorization before granting access.

Keep the Database Private

Databases generally should not accept connections directly from the public internet.

For an RDS-based design:

  • Place the RDS database in private subnets within a VPC.
  • Configure the database as not publicly accessible.
  • Allow inbound database traffic only from the application’s security group.
  • Let Lambda connect to the database through private network access when needed.

The security group rule should describe a specific relationship: the application can reach the database on its database port. It should not mean that “Anywhere on the internet can try its luck.”

A Lambda function attached to a VPC does not retain its default internet egress. Plan outbound access before deployment:

  • Use a NAT gateway when the function needs general internet access or access to public AWS service endpoints.
  • Prefer least-privilege VPC endpoints when the application only needs specific AWS services. For example, use an interface endpoint for Secrets Manager or Systems Manager Parameter Store when the function retrieves secrets or parameters, and use an S3 gateway endpoint when it accesses S3 from the VPC.

The selected endpoint or NAT design must also support the required DNS resolution, security group rules, and IAM permissions. Without an egress path, a Lambda function may reach RDS successfully but fail when it retrieves a secret or calls another AWS service.

DynamoDB is accessed through AWS service APIs rather than by opening a database port. Access remains controlled through IAM permissions and, where appropriate, VPC endpoints and network design.

4. Secure the First Architecture With Identity, Permissions, and Network Boundaries

Security is easier to handle when it is part of the initial design. Adding it later often requires untangling public access, shared credentials, and assumptions that have spread through the codebase.

Keep Application Users Separate From AWS Account Users

Application users are the people who use the task manager. They should not receive AWS IAM users or AWS account credentials.

Amazon Cognito can manage application sign-up, sign-in, password recovery, and token issuance. An external identity provider can also work if the organization already uses one.

A typical flow is:

  1. The user signs in through Cognito.
  2. Cognito issues tokens to the frontend.
  3. The frontend includes an access token with API requests.
  4. API Gateway validates the token before allowing the request to reach the backend.
  5. Lambda uses the user identity or claims to decide what data the user can access.

For example, a user should be able to read their own tasks but not retrieve every task in the database simply by guessing an API URL.

Use IAM Roles Instead of Access Keys in Application Code

AWS Identity and Access Management, or IAM, controls what AWS identities can do.

Lambda functions should use execution roles. ECS tasks should use task roles. Deployment systems should use separate deployment roles. Each role should receive only the permissions required for its job.

For example, a Lambda function that creates attachment upload URLs might need permission to write to one bucket and one object prefix:

s3:PutObject
Resource: arn:aws:s3:::task-app-attachments/attachments/*

A function that creates download URLs needs access only to the required object prefix and should authorize the task request before creating a GetObject URL.

Do not put long-term AWS access keys in frontend code, Git repositories, committed environment files, or client-side applications. If a browser can read a key, someone else can read it too.

Store Secrets Outside the Codebase

Database passwords, third-party API tokens, and signing secrets should not be hard-coded into Lambda functions or container images.

Use one of these managed options:

  • AWS Secrets Manager for secrets that need controlled retrieval and, where appropriate, rotation support
  • AWS Systems Manager Parameter Store for configuration values and secure parameters

The application role should be allowed to read only the secret or parameter it needs. Developers and deployment systems should not automatically receive access to every production secret.

For data encryption, AWS Key Management Service can manage encryption keys used by services such as S3, RDS, and Secrets Manager. Many AWS services provide encryption-at-rest options that integrate with AWS KMS.

Apply Network Boundaries and HTTPS

Use a VPC to separate private resources from public entry points.

For the task manager:

  • CloudFront and API Gateway can receive public HTTPS traffic.
  • The RDS database remains in private subnets.
  • Lambda receives permission to connect to the database.
  • Security groups restrict which resources can communicate with each other.

Use AWS Certificate Manager to manage HTTPS certificates for custom domains. HTTPS should be the standard path for both the frontend and API, not a feature added after user data is already moving through the system.

Turn On Logs and Make Them Useful

Amazon CloudWatch collects logs and metrics from services such as Lambda. At minimum, record enough information to diagnose failures without exposing sensitive user data.

Useful items to monitor include:

  • Lambda errors and duration
  • API Gateway error responses and latency
  • Database connections and storage behavior
  • Failed authentication attempts
  • Unusual access-denied errors
  • CloudFront and API request patterns when troubleshooting

Do not put passwords, access tokens, complete user records, or raw authorization headers in logs. Logs are useful, but they are not a safe place to store every piece of application data.

5. Deploy, Observe, and Simplify the Architecture Before Scaling It

The first production deployment should be small enough to understand. If you cannot explain how a browser request reaches the database and returns to the user, adding services will not make the system clearer.

Define Infrastructure as Code

Create AWS resources through an infrastructure-as-code approach instead of manually clicking through every console page.

Common AWS options include:

  • AWS CloudFormation
  • AWS CDK

Infrastructure as code makes it easier to recreate the architecture, review changes before deployment, and reduce configuration drift between environments.

A basic deployment definition might create:

  • A VPC and private database subnets
  • Security groups
  • An RDS database
  • An S3 bucket for frontend files
  • An S3 bucket for attachments
  • Lambda functions and IAM roles
  • API Gateway routes
  • Cognito resources
  • CloudFront distribution and DNS records

Keep the first infrastructure project readable. A large collection of reusable abstractions may help later, but a beginner should be able to trace each resource to a clear application need.

Create Separate Environments Where Practical

Use separate development and production environments. Depending on the team and budget, that may mean separate AWS accounts or clearly separated resources within one account.

The key requirement is that experimental work should not share the production database, secrets, or user data.

A useful baseline is:

  • Development: Used for local testing, previews, and integration checks
  • Production: Used by real users, protected by tighter permissions and more careful deployment controls

Name resources clearly and consistently. An ambiguous bucket or database name can make cleanup and incident response more difficult than necessary.

Deploy in a Deliberate Order

For the task manager, a sensible deployment sequence is:

  1. Create the VPC, subnets, security groups, and database.
  2. Create S3 buckets for frontend files and attachments.
  3. Configure Cognito and the application’s authentication flow.
  4. Deploy Lambda functions, IAM roles, and API Gateway routes.
  5. Connect the API to the database and test basic task operations.
  6. Publish the frontend build to S3.
  7. Configure CloudFront, OAC, the S3 bucket policy, and Route 53 for the application domain.
  8. Test sign-in, task creation, database persistence, private file uploads, and authorized attachment downloads.
  9. Verify logs, alarms, access permissions, recovery procedures, and cleanup procedures.

Test each connection in the request path. For example, confirm that the API can reach the database while a connection from the public internet cannot.

Establish a Database Recovery Baseline

Production task data needs a recovery plan, not just encryption.

For an RDS production database:

  • Enable encrypted RDS storage when creating the database.
  • Define an automated backup retention period that matches the application’s recovery needs.
  • Confirm that point-in-time recovery is available within that retention period.
  • Document how to restore the database and how the application will be pointed at the restored instance if needed.
  • Perform and record a restore test, including verification that task data can be read successfully from the restored database.

A backup that has never been restored is an assumption, not a proven recovery process. Repeat restore testing after important database, application, or recovery-procedure changes.

Monitor the Application From Its First Release

Set up CloudWatch dashboards, alarms, or at least a regular review process for the signals that matter.

Watch for:

  • API errors, especially repeated 4xx and 5xx responses
  • Lambda failures, timeouts, concurrency, and unusually long durations
  • Database connection pressure, slow queries, and RDS Proxy behavior if used
  • Authentication failures
  • Storage growth in S3
  • Health-check failures if using containers and a load balancer

Create billing budgets and alerts early. This is not a complete cost-management strategy, but it gives you a chance to spot unexpected usage before it becomes an unpleasant surprise.

Also remove resources created for experiments that are no longer needed. Test databases, unused load balancers, old snapshots, and forgotten development environments all need regular cleanup.

Add Complexity Only When a Measured Need Appears

Start with a working, secure path:

Browser
→ CloudFront
→ S3 frontend or API Gateway
→ Lambda
→ RDS and S3

Then let real observations guide changes.

For example:

  • Add a queue and background worker when a user action triggers slow processing.
  • Add RDS Proxy or adjust Lambda concurrency limits when database connection pressure appears.
  • Move a Lambda workload to ECS when runtime behavior no longer fits short request-driven functions.
  • Add read replicas, caching, or database tuning when measured database load justifies it.
  • Add more regions only when latency, resilience, or regulatory requirements make it necessary.

A beginner-friendly AWS architecture is not one that uses the fewest services at any cost. It is one in which every service has a clear job, permissions remain narrow, data is protected, recovery is tested, and the team can operate the system without guessing how its parts connect.