We have been pulling selected APIs out of our monolith into AWS for a while now, and most of that work has stayed deliberately boring. API Gateway sits in front, Cognito handles authentication when needed, request mapping shapes the payload, and a Lambda does the work. Most of those extractions are isolated enough that the risk stays understandable and the operational model stays simple.
We already made one pragmatic exception along the way for a high-volume event logging path. That one moved behind a Lambda function URL because API Gateway no longer matched the traffic shape or the cost profile. It was a useful reminder that cloud work is often less about dramatic redesign and more about learning where a platform stops fitting your assumptions.
The Constraint Was Small. The Impact Was Not.
The more interesting problem showed up when we needed to deploy a Lambda@Edge. Our pipeline already had a release model we trusted: build for staging, validate there, then promote the same thing to production with environment-specific values injected at deploy time.
Lambda@Edge broke one part of that model because it does not support environment variables. On paper, that sounds like a platform detail. In practice, it changed the meaning of promotion. If production needs different values and those values can no longer be injected at deploy time, then promotion may no longer be promotion. It may be another build step.
That was the real architectural issue for me. The Lambda code was not the hard part. The hard part was that a missing capability had quietly changed the trust model of the release process. Once that happens, you are no longer discussing only implementation. You are deciding what kind of release confidence you want to preserve.
The Tradeoff Was Really About Release Confidence
We had three credible options. A double build kept the deployment model operationally simple, but it weakened artifact identity because the thing validated in staging was no longer obviously the exact thing released in production. A header-based route kept a single build, but it moved environment selection into the runtime path and made configuration a behavior concern inside the function. Layers preserved a single tested artifact and kept configuration outside the code, but they added operational objects to version, attach, and keep compatible across environments.
That last point matters because it is where the layer approach stopped being a purely elegant answer. It was cleaner from a release-integrity perspective, but it also created more deployment surface area: separate layer lifecycle, version tracking, and another compatibility seam between code and config. The decision was not really "best architecture versus bad architecture." It was which kind of complexity the team would rather own.
Why We Still Chose the Double Build
In the end, the team chose a double-build approach, with one adjustment that made the risk easier to accept: build both environment-specific artifacts right away from the same source, in the same release window. That does not restore pure promotion, but it does reduce accidental drift and keeps the operational model straightforward.
I still prefer the layer approach in principle because it preserves the strongest separation between code and environment. But I do not think this was a case where only one answer was responsible. The double-build path was coherent, low-risk enough for the actual context, and simpler for the team to own day to day.
The Principle I Would Keep
What stayed with me is not really a Lambda@Edge lesson. It is a release-design lesson. If a platform constraint changes whether production still receives the same artifact you validated earlier, treat that as an architectural change, not as a minor pipeline inconvenience. It affects trust, rollback assumptions, and the kind of failure modes your team is accepting.
It also reinforced something more personal. It is easy to confuse preference with principle when both point in the same direction. If I had been deciding alone, I would likely have pushed for layers. But I was not deciding alone, and part of working well with a team is recognizing when a solution is not your preferred one and is still disciplined, coherent, and good enough to move forward with.
- Patrick