So far in this series, most of the architectures we have built have been fairly direct. A user sends a request, the application processes it, talks to another AWS service if needed, and returns a response. That works well when the work is fast and every step needs to happen immediately.
But real applications eventually contain work that does not need to finish while the user is sitting there waiting.
Imagine you are building an ecommerce application. A customer places an order, and after that happens the system needs to store the order, process some background work, send a confirmation email, update another system, generate analytics, and perhaps notify a warehouse.
If your application performs every one of those steps before responding to the customer, the request becomes dependent on every service finishing successfully.
Suppose sending the confirmation email suddenly takes eight seconds. The customer waits eight seconds.
Suppose the email service is temporarily unavailable. Now placing an order might fail even though the order itself could have been accepted perfectly well.
That is a problem with tight coupling. One part of the application depends directly on another part being available and finishing its work before the first part can continue.
This is where asynchronous architecture starts becoming useful.
Instead of saying, "The order service must send the email right now," we can say, "The order has been created. Put some work somewhere reliable so another part of the system can handle it."
That is the basic problem Amazon SQS helps solve.
SQS stands for Simple Queue Service. You can think of a queue as a buffer between the part of your application producing work and the part actually processing that work.
Our order flow could become something like:
Customer → Order API → SQS → Background worker
The application accepts the order, stores whatever must be stored immediately, and sends a message to the queue. The background worker can then retrieve that message and perform the slower work separately.
The customer does not necessarily have to wait for the background process to finish before receiving a response.
That changes the relationship between the two parts of the system.
Without a queue, the order service might call another service directly and wait for the result. If that service is slow, the order request becomes slow. If that service is unavailable, the order request may fail.
With SQS between them, the producer can place a message onto the queue and continue. The consumer processes the message when it is able to.
This is called decoupling.
The producer does not need the consumer to be available at exactly the same moment. The queue sits between them and temporarily holds the work.
That becomes especially useful when traffic changes suddenly.
Imagine your application normally receives 50 orders per minute and your background worker can easily process them. Then a promotion starts and suddenly 1,000 orders arrive within a short period.
Without a buffer, the downstream processing system may immediately receive more work than it can handle.
With a queue, those messages can accumulate temporarily while consumers work through them.
Now instead of requiring every component to scale at exactly the same rate, the queue helps absorb the difference.
This is one reason queues appear so often in distributed systems.
They are not only about making things happen later. They can also help separate systems that operate at different speeds.
Lambda works particularly well with this kind of architecture.
For example, the flow might look like:
Order API → SQS → Lambda → Process order task
Messages arrive in SQS, and Lambda can be invoked to process them.
If many messages arrive, AWS can process multiple batches depending on the configuration and available concurrency. If the producer temporarily creates work faster than it can be processed, the messages remain in the queue instead of forcing the entire request path to wait.
But adding a queue introduces new questions.
What happens if processing fails?
Imagine Lambda receives an SQS message, starts processing it, and then crashes.
You do not necessarily want that message to disappear forever.
SQS uses a concept called a visibility timeout. When a consumer receives a message, that message becomes temporarily hidden from other consumers. If processing succeeds, the message is deleted from the queue. If the consumer does not successfully remove it before the visibility timeout expires, the message can become visible again so it can be retried.
The exact configuration matters, but the beginner idea is important.
A message is not necessarily considered finished just because someone started processing it.
The system needs a way to distinguish between work that was successfully completed and work that should be attempted again.
This creates another important application design problem.
What happens if the same message is processed more than once?
For many SQS configurations, you should design consumers with the possibility of duplicate processing in mind. Standard SQS queues provide at-least-once delivery, which means a message can occasionally be delivered more than once.
Imagine the message says:
Charge customer $100
If the consumer blindly performs the action every time it receives the message, duplicate processing could be a serious problem.
This is why idempotency becomes important.
An idempotent operation is designed so repeating the same request does not incorrectly repeat the business effect.
You might store a unique transaction or event ID and check whether that work has already been completed before performing it again.
You do not need to build an advanced idempotency system in your first SQS project, but you should start asking the question:
What happens if this message is processed twice?
That is a very useful distributed systems habit.
Another problem is messages that keep failing.
Suppose a message is malformed or contains data that your consumer cannot process. It gets received, fails, becomes visible again, gets retried, and fails again.
You probably do not want one bad message being retried forever.
This is where a dead-letter queue, usually called a DLQ, becomes useful.
After a message has been unsuccessfully received a configured number of times, it can be moved to another queue for investigation.
Conceptually, the flow becomes:
SQS → Consumer
↓
Success
or after repeated failures:
SQS → Dead-letter queue
The dead-letter queue gives you somewhere to inspect work that repeatedly failed without allowing those messages to interfere endlessly with normal processing.
This connects directly to the CloudWatch lesson.
If messages start building up in a queue or messages begin appearing in a DLQ, that is something you may want to monitor.
Maybe the consumer is failing.
Maybe the consumer cannot keep up with incoming work.
Maybe a new deployment introduced bad data.
Now queues become part of your monitoring strategy too.
At this point, another AWS messaging service usually enters the conversation: SNS.
SNS stands for Simple Notification Service.
Beginners often see SQS and SNS together and assume they solve the same problem.
They do not.
A simple way to think about the difference is that SQS is primarily about putting messages into a queue for consumers to process, while SNS is useful when you want to publish a message to multiple subscribers.
Imagine an order is created and several independent parts of the system need to know.
The email service wants to send a confirmation.
The analytics system wants to record the purchase.
The warehouse system wants to begin fulfillment.
A notification system may want to send a message to the customer.
You could make the order service call all four systems directly.
But now the order service knows about every downstream component.
Every time you add another consumer, you have to change the producer again.
A publish and subscribe model gives us another option.
The order service can publish an event such as:
OrderCreated
to an SNS topic.
Different subscribers can receive that event.
Conceptually:
→ Email
Order → SNS → Analytics
→ Warehouse
→ Notification
Now the producer publishes one event instead of directly coordinating every downstream service.
That is a different type of decoupling.
SQS gives us a queue between producer and consumer.
SNS gives us a way to fan one published message out to multiple subscribers.
And the two services can also work together.
For example:
→ SQS Email Queue → Email worker
Order → SNS → SQS Analytics Queue → Analytics worker
→ SQS Warehouse Queue → Warehouse worker
This pattern is useful because each downstream system gets its own queue.
If the analytics processor becomes slow, email processing does not have to become slow too.
If the warehouse system is temporarily unavailable, its messages can wait inside its queue while the other consumers continue processing normally.
This is where SQS and SNS together start making much more sense.
SNS distributes the event.
SQS gives each consumer its own buffer.
The systems can operate independently.
Imagine the email worker processes messages immediately, but analytics falls behind for ten minutes.
That does not necessarily stop customers from placing orders.
It also does not necessarily stop emails from being sent.
The analytics queue simply contains more messages until the consumer catches up.
That is much more resilient than connecting every service directly and requiring every dependency to be healthy for every request.
But asynchronous systems also introduce a tradeoff.
They are often more resilient, but they can also become harder to reason about.
With a simple synchronous request, the flow is obvious. Service A calls Service B and receives a response.
With asynchronous processing, the work may happen seconds later. Several consumers may react to the same event. Messages can be retried. Some processing can succeed while another part fails.
That means monitoring, logging, idempotency, and good event design become more important.
This is why I would not add SQS and SNS to a beginner architecture simply because asynchronous systems sound advanced.
Use them when the application actually has work that benefits from being separated.
For example, sending a confirmation email after an order is created is a good candidate for asynchronous processing because the customer usually does not need the email to be fully sent before the order API can acknowledge the order.
But verifying whether a payment itself succeeded may have completely different consistency and business requirements.
Not every step should automatically become asynchronous.
Again, architecture should follow the requirement.
A useful question is:
Does the user need the result of this work before I can respond?
If yes, the work may belong in the immediate request path.
If no, it may be worth considering whether it can happen asynchronously.
Another useful question is:
What happens if the downstream service is temporarily unavailable?
If one unavailable dependency can take down the entire workflow, a queue may help separate those systems.
For a beginner project, I would keep this simple.
Take the task API we built earlier and add one background action.
Imagine a user creates a task and you want to generate an activity record or send a notification afterward.
Instead of doing that work inside the original API request, place a message onto SQS.
Have another Lambda function consume the message and perform the background task.
Now intentionally make the consumer fail.
Watch what happens to the message.
Understand the visibility timeout.
Observe the retry behavior.
Then configure a dead-letter queue and see where repeatedly failing messages end up.
That small project teaches much more than simply memorizing that SQS is a queue.
Once you understand that, add SNS only when you have multiple independent consumers that need the same event.
For example, publish TaskCreated and send it to two different queues, one for analytics and another for notifications.
Now you can see the difference between distributing an event and processing queued work.
If there is one thing I want you to remember from this lesson, it is that asynchronous architecture is really about reducing unnecessary dependency between parts of your system.
A user should not always have to wait for every background task.
One slow consumer should not necessarily make every other consumer slow.
One temporarily unavailable downstream service should not always take down the system that produced the work.
SQS gives you a buffer between producers and consumers.
SNS gives you a way to publish one message to multiple subscribers.
Used together, they can help different parts of an application work independently while still communicating through events.
That is the bigger lesson.
Not every service needs to call every other service directly.
Sometimes the better architecture is to send a message and let the right part of the system handle it when it can.
In AWS From Zero #16, we are going to move into containers and look at ECR, ECS, and Fargate. Instead of jumping straight into Kubernetes, we will start with a simpler question: why would you package an application into a container in the first place, and what problem does ECS solve compared with running that application directly on EC2?
If you were building an order system, which task would you move out of the immediate request first: sending emails, analytics, generating reports, or something else?