AWS makes it incredibly easy to add things.
Need containers? ECS or EKS.
Need asynchronous processing? SQS, SNS, EventBridge, Step Functions, Kinesis.
Need compute? EC2, Fargate, Lambda, Batch.
That breadth is one of AWS’s greatest strengths.
It is also one of the easiest ways to overengineer a system.
After years of operating production infrastructure, I’ve found that one of the most useful AWS skills isn’t knowing more services.
It’s knowing when not to use them.
The question eventually changes from:
What AWS service could solve this?
to:
Does adding another service actually make the system better?
That distinction matters.
Complexity Has a Cost#
When people discuss AWS cost, they usually mean the bill.
But every service also adds some combination of:
- implementation work
- IAM and networking
- monitoring and alerting
- deployment logic
- failure modes
- documentation
- operational knowledge
- another thing someone has to debug during an incident
An architecture can look beautifully decoupled on a diagram and still be miserable to operate.
That doesn’t mean complexity is bad.
It means complexity needs to earn its place.
My Architecture Test#
Most of my decisions eventually come down to four things:
- Cost
- Implementation effort
- Operational overhead
- Risk
I’m not assigning numerical scores to each one. It’s simply the framework I use to think through the tradeoff.
What does the new service cost?
How much work does it take to implement correctly?
Who has to operate it six months from now?
And most importantly: what risk does it remove, or introduce?
I’m not interested in building the simplest architecture possible.
I want the simplest architecture that safely satisfies the requirement.
That is a very different goal.

ECS Instead of EKS#
Our frontend workloads run on ECS Fargate.
Could we run them on EKS?
Absolutely.
But the question is not whether Kubernetes could run the workload.
The question is:
What problem would EKS solve for us that ECS isn’t already solving?
We need to deploy containers, scale them, load-balance them, connect them to IAM and networking, and monitor them.
ECS does that well.
Moving to EKS would give us more flexibility, but it would also introduce more platform surface area: Kubernetes configuration, cluster management, networking decisions, add-ons, upgrades, and additional operational knowledge.
There are environments where that trade is absolutely worth it.
For ours, ECS already solves the problem.
So we use ECS.
EC2 Is Not a Failure to Containerize#
The reverse is also true.
Once a team adopts containers, there’s a temptation to containerize everything.
Our Next.js frontend is a natural fit for ECS.
Magento is not nearly as clean.
Magento brings PHP-FPM, Nginx, system dependencies, extensions, scheduled jobs, and a more involved runtime environment.
We run it on EC2 instances built from controlled AMIs and deployed through an Auto Scaling Group.
That gives us a known server state:
- PHP version
- extensions
- Nginx configuration
- application dependencies
- monitoring agents
- system configuration
Could Magento run in containers?
Yes.
But the useful question is not:
Can we containerize it?
It’s:
What do we gain by doing so?
Until the answer becomes compelling, EC2 is simply the compute model that fits the workload.
Sometimes Adding a Service Removes Complexity#
This is why I don’t like rigid rules such as “don’t use Lambda.”
We ran into a good example after moving more Magento infrastructure onto AWS Graviton.
ARM64 was a great fit overall, but some scheduled Magento workloads depended on libraries that became unnecessarily awkward to maintain in the main server environment.
We could have made the Magento AMI more complicated to support those jobs.
Instead, we moved some of them into Lambda.
That added another AWS service, but it actually reduced the complexity of the overall system.
| |

Yes, Lambda introduced IAM, logging, deployment, and another component.
But it removed specialized dependencies from every Magento server.
The metric isn’t how many AWS services you’re using.
It’s how complicated the system is as a whole.
Use the Compute Model That Fits the Work#
That same reasoning cuts the other direction.
Not every background task belongs in Lambda.
If work is long-running, resource-heavy, has a complicated runtime, or naturally belongs inside an existing worker environment, Lambda may make the design worse.
Sometimes the correct architecture really is:
| |
The worker might run on ECS or EC2.
Start with the workload characteristics, then choose the compute model.
Don’t start with:
I want to use Lambda. How can we make this fit?
Don’t Reach for DynamoDB Before You Know How You’ll Query It#
DynamoDB is an excellent database, but it rewards understanding your access patterns early.
Partition keys, sort keys, and indexes are closely tied to how the application reads and writes data.
So one of the first questions should be:
What are our access patterns?
If the answer is:
We’re still figuring that out.
then I would be cautious about building an elaborate DynamoDB model.
If the data is relational and the query requirements are still evolving, RDS or Aurora may be much easier to operate.
When the workload has predictable key-based access patterns, DynamoDB can be exceptional.
Again, let the workload choose the service.
Sometimes SQS Is Enough#
Event-driven systems are another easy place to add unnecessary layers.
If one producer needs to hand work to one consumer asynchronously, this may be all you need:
| |
You already get:
- buffering
- retries
- independent scaling
- fault isolation
- dead-letter handling
EventBridge becomes valuable when you actually need richer routing, multiple consumers, broader event integration, or domain-event patterns.
But if the requirement is simply:
Put this work somewhere safe until a worker can process it.
a queue is hard to beat.
Multi-Region Is a Requirement, Not a Trophy#
Multi-region architectures look impressive.
They also add substantial complexity:
- replication
- consistency
- routing
- failover
- synchronized deployments
- regional dependencies
- failback
- testing
Before building active-active infrastructure, I want to understand the real recovery requirement.
What is the RTO?
What is the RPO?
Can the business tolerate 30 minutes of downtime?
Five?
Do we need live traffic in two regions, or do we need excellent backups, infrastructure as code, and a tested recovery process?
Those answers matter far more than whether the architecture diagram has two AWS regions on it.
A Module Boundary May Be Enough#
The same principle applies to microservices.
A well-structured application can have clear boundaries between catalog, checkout, pricing, inventory, and accounts without turning each one into a separate distributed system.
Once you split those modules into independent services, you also introduce:
- network communication
- retries and timeouts
- service authentication
- deployment pipelines
- distributed tracing
- more failure modes
Sometimes that’s justified.
Independent scaling, ownership, deployment, or reliability requirements may make the split worthwhile.
But a module boundary is often enough until one of those requirements exists.
A monolith is not automatically badly designed.
Sometimes the Complexity Really Is Necessary#
There is a danger in taking simplicity too far.
Sometimes a system is complicated because the business problem is complicated.
Separating infrastructure across markets is a good example.
At first glance, that separation can look excessive.
But there are often real requirements behind it: inventory, fulfillment, billing, operational boundaries, and failure isolation.
Could we make the diagram look simpler by collapsing everything together?
Probably.
Would the system actually be better?
No.
There are architectures where I can say:
This is more complicated than I would normally build.
and immediately follow it with:
But removing that complexity would violate a real requirement.
That isn’t accidental overengineering.
That’s the cost of the problem.

AWS Reference Architectures Are Starting Points#
I don’t think the lesson here is that AWS reference architectures are too complicated.
AWS has some of the best cloud architects in the industry, and their reference designs are extremely useful.
But their job is to demonstrate patterns that work across a wide range of customers and requirements.
My job is narrower.
I need to understand why each component exists and decide whether I have the same requirement.
That’s the difference between copying an architecture and designing one.
The same goes for technologies that are attractive because they are interesting to learn.
Production is a very expensive place to add Kubernetes, Kafka, microservices, or any other technology purely because it would be good experience.
Learn them.
Prototype them.
Use them when they solve the problem.
The Question I Keep Coming Back To#
Before introducing a major component, I want to be able to finish this sentence:
We are adding this because…
The answer should describe a problem.
Good:
We’re adding SQS because incoming work can exceed what the consumer can safely process, and we need buffering and retries.
Good:
We’re moving this job to Lambda because its dependencies don’t belong on every Magento server.
Good:
We’re separating market infrastructure because the business requires an independent operational boundary.
Less convincing:
We’re adding EKS because Kubernetes gives us more flexibility.
Flexibility for what?
We’re moving to DynamoDB because it scales.
Which access pattern requires it?
Keep asking why.
Eventually the architecture either becomes justified or the rationale falls apart.
Both outcomes are useful.
A Small Complexity Budget#
I think of architecture as having a complexity budget.
Some complexity is imposed by the business and cannot be removed.
What I can control is how much additional technical complexity I put around it.
I’m happy to spend that budget when it buys something meaningful:
- fault isolation
- required scalability
- better security
- lower operational risk
- necessary recovery characteristics
- cleaner ownership boundaries
- meaningful cost savings
- removal of worse complexity somewhere else
But complexity should purchase something.
Otherwise we’re just paying for it.
Four Questions Before Adding Another Service#
Before introducing another AWS service, I would ask:
What problem are we solving?#
Describe it without naming an AWS service.
Can something we already operate solve it?#
Existing technology has one enormous advantage:
the team already understands it.
What complexity does this remove versus add?#
Look at the whole system, not just the new component.
What happens if we don’t build it?#
If the answer is “not much,” that’s useful information.
If the answer is “we risk losing orders,” that’s useful too.

Boring Infrastructure Is Often Great Infrastructure#
Some of my favorite production systems are boring.
Not outdated.
Not neglected.
Boring.
They use mature patterns.
Engineers understand how requests move through them.
Failures are observable.
Recovery procedures are known.
New engineers can reason about them without weeks of archaeology.
AWS gives us an enormous toolbox.
Knowing that toolbox deeply is valuable.
But the goal isn’t to use every tool.
It’s to understand the tradeoffs well enough to leave most of them in the toolbox.
Sometimes the right answer is Kubernetes.
Sometimes it’s Lambda.
Sometimes it’s DynamoDB or multi-region.
And sometimes the best architectural decision you’ll make all week is:
No. We don’t need that.




