AWS Contact Center
How Booking.com manages Amazon Connect Customer flows as code at scale
Introduction
Booking.com is one of the world’s largest online travel platforms. Its customer service organization runs on Amazon Connect Customer, and a voice engineering team supports the contact experience across several lines of business and locales.
Booking.com manages Amazon Connect Customer flows as code at scale, and its flow designers still work in the Amazon Connect Customer console. That is where they build the flows, flow modules, and agent workspace views that shape every traveler’s conversation. Booking.com’s engineering organization has a different and equally valid requirement: every production change must ship as code.
The gap was expensive. A flow authored in the console did not automatically land in Git or in downstream environments. Exported by hand, its JSON was full of environment-specific Amazon Resource Names (ARNs) that could not be promoted as-is. Those references covered queues, AWS Lambda functions, Amazon Lex bots and prompts. Engineers hand-edited references or maintained brittle scripts, and the repository drifted from what designers had published.
Automating the export was straightforward; the ARN references inside the exported flow were not. The most effective fix turned out to be changing how designers compose flows rather than building a cleverer rewriter.
The result is faster, safer promotion with no manual JSON editing. Booking.com measured what this removed. Across 90 flows and the 242 change events that followed them over about six months, the work of getting a finished design out to all three environments — not the design work itself, which is unchanged — fell from 1,164 engineer-hours to 78.
This post covers both layers: the composition discipline that shrinks the problem, and the event-driven pipeline that automates what remains. Using the AWS Cloud Development Kit (AWS CDK) instead? See the companion post Managing Amazon Connect flows as Code with AWS CDK, which approaches the same problem code-first. This approach is console-first: designers keep the GUI, and code is generated from what they publish.
Why flows resist promotion
Exporting flows so they live in Git is the straightforward half. The hard half is what the exported JSON contains.
A flow references queues, modules, prompts, hours of operation, data tables, views, Lex bots, and Lambda functions by ARN. Every one of those ARNs is specific to the instance where it was authored. Promote that JSON unchanged and it fails to deploy — or worse, deploys and points at nothing.
The instinctive fix is to rewrite every ARN on the way through. That works, and this solution does it. However, on its own it treats a design problem as a tooling problem. AWS prescriptive guidance for IVR design on Amazon Connect Customer names eliminate static design as its first architectural principle:
A common mistake developers make when they design an IVR system is to introduce static information or hard-coded logic within the flow.
A hard-coded ARN is static design, and cross-environment friction is one symptom. The other costs more: a flow with an embedded queue ARN serves one line of business, so you maintain near-identical copies of the same logic. Amazon Connect Customer performance efficiency best practices make the same point from the scaling side, recommending many phone numbers map onto few flows.
So Booking.com attacked it from both ends. Designers compose flows so that most references resolve at runtime instead of being embedded, which reduces the problem to a handful of static references per flow. The pipeline substitutes what remains. The composition discipline is what makes the automation tractable.
Overview of the solution
Dheeraj Yadav, Voice Engineer at Booking.com, describes the balance the team needed to strike: “Our flow developers need to keep designing in the Amazon Connect Customer console, but our engineering standards require every production change to ship through the same CI/CD pipeline as the rest of our code. Working with AWS, we designed for both constraints — following their guidance on flow composition while keeping our promotion process unchanged. Our designers did not have to change how they work, and our engineers did not have to relax how we ship.” The solution has two layers.
Compose for portability. Designers use dynamic references so queues, prompts, and target flows are looked up at contact time, shrinking the number of ARNs any flow carries.
Automate the round trip. An event-driven pipeline exports every console change as a portable template. It then resolves the residual references per environment:
- Amazon Connect Customer — where designers create and update resources (the source of change).
- AWS CloudTrail — records the Connect management API calls a console save generates.
- Amazon EventBridge — rules match those events and invoke the exporter.
- AWS Lambda — fetches the changed resource, rewrites remaining environment-specific references into portable placeholders, and writes a deterministic template.
- Amazon S3 — stores the exported .json.tpl templates, versioned and encrypted.
Downstream, Terraform modules render those templates, injecting environment-appropriate ARNs at deploy time. The result is GUI-driven design on the front end and promotion driven by infrastructure as code (IaC) on the back end, connected automatically.

Figure 1. Overview of the flows-as-code pipeline.
A note on portability: this solution targets Terraform, whose templatefile() function and ${var} placeholders are core to making templates environment-agnostic. The architecture transfers to AWS CloudFormation or the AWS CDK; the substitution mechanism would need adapting.
Composing flows for portability
Not every reference should be dynamic. Two questions decide it.
Is there one correct value per environment for the life of a deployment? If yes, substitute it at deploy time. A missing placeholder fails terraform apply, so the pipeline stops and nothing ships. Replace that with a runtime lookup and the same mistake surfaces on a live contact instead. You have traded a build-time check for a production failure mode and gained nothing.
Does one deployed flow need to reach different targets on different contacts? Then no single deploy-time value exists, and a dynamic reference is the only option.
| Reference | Approach |
|---|---|
| Varies per contact (line of business, locale, segment) | Dynamic — no single value exists |
| Shared across many flows | Dynamic — avoid stamping it into every artifact |
| Must change without a release (unplanned closures, feature flags) | Dynamic — operational, not deployment, tempo |
| Single-valued per environment | Substitute — keeps apply-time validation |
| Block offers no dynamic option | Substitute — no choice |
In practice a Set working queue block selects its queue from a looked-up value. A Play prompt block reads localized text, and Transfer to flow targets a value resolved at runtime. Booking.com keeps those environment-specific values in Amazon Connect Customer data tables and manages the records as code, so both sides of the contract live in Git.
Data tables matter here because the flow reads them natively, through the Data table block. The established version of this pattern keeps configuration in Amazon DynamoDB behind a Lambda function. Every flow then still carries one static Lambda ARN and pays a cold start on lookup. Reading a data table directly removes both, and records are available immediately with no redeployment.
The block’s Evaluate action queries by primary attribute and returns the attributes you select, into the $.DataTables namespace:

Figure 2. How a dynamic reference resolves at contact time.
Because the key is the dialed number, one flow serves many callers. Adding a line of business or locale becomes a new row, not a new flow.Three caveats to respect:
The residual is irreducible. Whatever performs the lookup is itself a static reference that must be substituted. Dynamic composition shrinks the surface; it never reaches zero.
Degrade gracefully. The prescriptive guidance is explicit: keep “a default branch with a minimal set of static messages that can be used in the event of database or query failures.” A failed lookup should play a fallback prompt, not drop the contact.
Watch the value shape. Set working queue with Set dynamically expects a queue ID, not a name or an ARN, and getting it wrong fails at runtime rather than at deploy time.
Prerequisites
- An AWS account and an Amazon Connect Customer instance with at least one flow
- The companion sample needs no existing instance; it creates two of its own. Check the Region has room for two under the instance quota, which defaults to two and counts instances you already have
- No CloudTrail trail is required. Mutating management events such as UpdateContactFlowContent reach Amazon EventBridge by default; the trail prerequisite applies only to read-only events
- Terraform v1.6 or later, with the awscc provider (v1.7+) and the aws provider (v6+)
- An Amazon S3 bucket for exported templates, or let the module create one
- IAM permissions to create Lambda functions, EventBridge rules, S3 objects, and Amazon CloudWatch log groups
- A Lambda runtime of Python 3.12 with boto3 1.42.81 or later, required for the Amazon Connect Customer data tables API
- Familiarity with Amazon Connect Customer flows and Terraform module structure
The export pipeline
Detecting the change. A console save calls a Connect management API. CloudTrail records it, an EventBridge rule matches on event name, and the exporter Lambda is invoked. Define one rule per resource type so the exporter reacts only to the events that matter.
Making the reference portable. The exporter describes the changed resource, then maps each remaining environment-specific reference to a placeholder. Each key is the resource type plus a Base64 encoding of the resource’s stable name, made safe for Terraform identifiers:
Encoding the name rather than the ID is what makes this work across environments: the same queue has a different UUID in Dev and Production, but the same name.
Keeping Git clean. Before writing, the exporter strips non-deployable fields and serializes with sorted keys and fixed indentation, so the same flow always serializes identically and a diff reflects a real change rather than key reordering. A change detector compares against the existing S3 object and skips the write when nothing changed.
Promotion: resolving the residual
This is the part that makes promotion work. Terraform renders each template with templatefile(), passing a map that resolves every placeholder to the real value for the target environment. The map is built by applying the same encoding function to the names of resources Terraform already manages:
The exporter and consumer never communicate; they agree because both apply the same function to the same stable name. Three consequences worth planning for:
Every referenced resource must be code-managed in the target environment. The map is built from resources Terraform owns. A designer who references a console-created queue produces a template whose placeholder has no key in QA, and the apply fails. This is the real constraint the design imposes on designers.
The encoding is a contract in two languages. It lives in Python in the exporter and HCL in the consumer modules. If either drifts, every placeholder silently stops resolving. Pin both with a shared fixture of names and expected keys.
Flows and modules are both deployed with the awscc provider, using awscc_connect_contact_flow and awscc_connect_contact_flow_module, so newly released Connect Customer resources are reachable as soon as AWS CloudFormation supports them.
Lessons learned at Booking.com
Composition beats cleverness. Booking.com started by making the rewriter smarter. The bigger win came from reducing what needed rewriting. Every reference a designer resolves at runtime is one the pipeline never has to get right. Across the flows and change events measured earlier, that combination is what removed about 93 percent of the rollout effort — the rebuild-by-hand work in three separate Connect consoles that the exporter replaces.
Determinism is what makes IaC reviewable. Sorting keys and normalizing formatting turned noisy, unreviewable diffs into meaningful pull requests. It was the biggest adoption lever the team cited.
Don’t make dynamic the default. A runtime lookup for a value that only ever changes at deploy time trades a pipeline failure for a customer-facing one. Reserve dynamic references for cases where deploy time genuinely cannot answer the question.
Design for reference ambiguity up front. Display names collide across resource types. Detecting that and falling back to the literal name avoided a class of subtle cross-environment bugs.
The change reviews as a readable diff:
One line changed, no environment-specific ARNs, no reordered keys.
Cleaning up
Destroy the consumer modules first, then the exporter, then empty and delete the versioned S3 export bucket. Full teardown steps are in the sample repository.
Note: destroying consumer-module resources deletes the associated flows from your Connect Customer instance. Verify no active traffic depends on them first.
Conclusion
Booking.com closed the gap between how designers want to work and how engineering needs to ship. The console became the design surface, and IaC the promotion mechanism. What made promotion reliable was composing flows so that fewer references are embedded in the first place. That had a second payoff: the same flow now serves multiple lines of business instead of being copied per LOB.Count the ARNs in one of your own exported flows. That number tells you how much of your promotion problem is tooling and how much is design.
Then try it. The sample repository builds two Connect Customer instances in one account, seeds flows into the first, exports them, and promotes them into the second with a single terraform apply and no manual JSON editing. Its README carries the step-by-step walkthrough, prerequisites and teardown. It needs no phone number, so its routing_config table keys on a fixed value rather than the dialed number of Figure 2. For the flow-language reference behind the dynamic blocks used here, see Amazon Connect flow block types in the Amazon Connect Administrator Guide.
For more on the design principles behind this, see Designing the foundation of a dynamic and modular IVR experience in AWS Prescriptive Guidance.
Ready to transform your customer service experience with Amazon Connect Customer? Contact us.