Speeding up Application Delivery by having Full Stack Preview
How every pull request at a non-profit organisation got its own working copy of the whole application, so testers, engineers and product could try a change before it was merged.
For a long time, the first place anyone could see a change running was the shared development environment. By then the code was already merged. If something was wrong, the fix started again as a new pull request and waited for the next deploy.
At the non-profit organisation where I work, I changed that. Every pull request now gets its own full stack preview: the frontend, the API, the functions and the event-driven parts behind them, deployed just for that pull request. Testers, engineers and product can open it, use it and break it before anything is merged.
Our delivery cycle got about 40% shorter, and we were closing more tickets and shipping more features than before. This post covers why we built it, how it works and the trade-offs we chose.
The problem: testing after the fact
The application is serverless. The frontend is hosted on AWS Amplify, it calls APIs on Amazon API Gateway backed by AWS Lambda, and a lot of the work happens asynchronously in event-driven functions.
Before previews, every change followed the same path: open a pull request, get it reviewed, merge it, wait for it to deploy to development, then test it there. A problem found at that last step cost a full extra lap: a new pull request, another review, another merge and another deploy.
That loop caused friction everywhere:
- Testers saw changes late. Several changes usually landed in development together, so when something broke it took time to work out which one did it.
- One bad change blocked everyone. Development was shared, so a broken merge stopped other people’s testing too.
- Product saw features after they were built. Changing direction at that point is expensive, so feedback was often deferred.
- Engineers stopped experimenting. If the only way to show an idea was to merge it into development, it was easier not to try. This was the cost I cared about most: the process discouraged new ideas without anyone deciding it should.
What a full stack preview is
Frontend previews are common. Amplify can build a preview of the frontend for every pull request out of the box. On its own that wasn’t enough for us. Most changes touch an API or an event handler as well, and a frontend preview pointed at the development backend can’t show backend code that hasn’t been merged yet. You only see half the change.
So each pull request gets the whole application:
- a frontend preview on Amplify, built from the pull request’s code;
- its own API Gateway API and Lambda functions, built from the same commit;
- its own event bus and consumers, so an event raised in a preview is handled by that preview’s code and never leaks into development or another preview;
- a connection to the shared development database, so there’s realistic data to work with from the first minute.
How it works
The new AWS platform and CI/CD were built around one idea: any developer should be able to make any kind of change and see it running in isolation, without asking anyone for an environment.
- Everything is named after the pull request. Stacks never collide, and every resource in a preview is easy to find and to delete.
- The pipeline does the work. When a pull request opens, CI deploys the backend stack for it, and the Amplify preview for the same pull request points at that stack’s API. Amplify passes the pull request number to its build as
AWS_PULL_REQUEST_ID, which makes that wiring simple. - Every push updates the preview. Frontend and backend move together, so the preview always matches the code under review.
- Closing the pull request cleans up. Amplify deletes its preview when the pull request closes, and the pipeline deletes the backend stack at the same point.
- 01PR openedThe pipeline deploys a full stack named after the pull request.
- 02Links postedThe preview URLs appear on the pull request for anyone to open.
- 03Every push redeploysNew commits update the same preview, frontend and backend together.
- 04Test and playTesters, engineers and product try the change in a working app.
- 05Merged or closedThe change goes to development already tested.
- 06Torn downThe preview stack is deleted, so nothing is left running.
Why the database is shared
Giving every preview its own database looks cleaner on paper. We chose not to, for two reasons:
- Testing needs realistic data. A new database starts empty, and seeding one per pull request is slow and drifts from reality.
- Previews have to be fast and cheap. The database is the slowest and most expensive part to create. Everything else in a serverless stack costs close to nothing while idle, so the per-pull-request part stays cheap.
The rule of thumb: isolate everything that changes with the code, and share what is slow and expensive to create.
Sharing does come with rules. Development and every open preview read the same data at the same time, so database changes have to be backward compatible: add the new field or collection first, move the code over, and only remove the old one once nothing uses it. Data a preview writes is visible to everyone else in development, so test data is treated as shared.
What changed
Delivery cycle time, indexed (before = 100)
40% shorter
- Delivery cycle about 40% shorter. Most of the gain came from fixing problems inside the pull request instead of after merge.
- More tickets closed and more features shipped than before previews.
- Feedback moved before the merge. Testers test one change in isolation, and product can see and react to a feature while it is still a pull request.
- Development stayed more stable, because fewer broken changes reached it.
- Experiments became cheap. Showing an idea now means opening a pull request and sharing a link.
What I’d tell someone building this
- Preview the whole stack, not just the frontend. Most of the value is in seeing the backend change running too.
- Make it automatic. No tickets and no requests. If someone has to ask for a preview, people won’t.
- Tie the preview’s life to the pull request. Create it on open, update it on push, delete it on close. Then nobody has to remember to clean up.
- Decide what to share on purpose. Isolate what changes with the code. Share what is expensive to create, and write down the rules that sharing needs.
- Watch the limits. Each Amplify pull request preview counts towards the quota of 50 branches per app, so stale pull requests need closing.
The tools here are ordinary AWS services. The change that mattered was moving testing to the point where fixing something is cheapest: before the merge.