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.

Architecture diagram. A designer saves a flow in the Amazon Connect Customer console. AWS CloudTrail records the API call and Amazon EventBridge invokes an AWS Lambda exporter. The exporter writes a portable JSON template to Amazon S3. CI/CD opens a pull request; on merge, Terraform renders the template and deploys to Dev, QA, and Production.
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:

Data table: routing_config
  Primary attribute: DialedNumber = $.SystemEndpoint.Address
  Query name:        RoutingLookup
  Return:            QueueId, GreetingText

Set working queue → Set dynamically → $.DataTables.RoutingLookup.QueueId

Diagram. Two contacts on different dialed numbers arrive at a single shared flow. The flow queries an Amazon Connect Customer data table using the dialed number as the key. The table, managed as code in Git, maps each dialed number to a queue ID and a greeting. Each contact is routed to a different queue through the same flow.
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:

def _encode_name(name):
    """Connect resource names allow spaces and punctuation that Terraform
    identifiers do not. Base64 is total and collision-free over that input.
    The three substitutions make it a legal Terraform identifier."""
    encoded = base64.b64encode(name.encode("utf-8")).decode("ascii")
    return encoded.replace("+", "-").replace("/", "_").rstrip("=")
# "retail-support_en" -> ${queue_cmV0YWlsLXN1cHBvcnRfZW4}

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:

locals {
  # Both sides derive the same key from the resource's stable name via one
  # shared function (the sample factors it into a `placeholder-keys` module).
  # No registry, no shared state — just an agreed pure function.
  template_vars = merge(
    { for name, arn in local.lambda_function_mapping :
      "lambda_${local.tf_safe(name)}" => arn },
    { for name, id in local.queue_mapping :
      "queue_${local.tf_safe(name)}" => id },
    # ... contact_flow, contact_flow_module, routing_profile, view
  )

  rendered = {
    for key, flow in local.flow_files :
    key => templatefile(flow.path, local.template_vars)
  }
}

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:

  {
    "Parameters": {
-     "QueueId": "${queue_cmV0YWlsLXN1cHBvcnRfZW4}"
+     "QueueId": "${queue_cmV0YWlsLXByaW9yaXR5X2Vu}"
    },
    "Type": "UpdateContactTargetQueue"
  }

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.


About the authors

Akash Kumar Sinha

Akash is a DevOps Consultant and GenAI Ambassador at AWS, where he helps customers transform their cloud operations through containerization, feature management, and modern delivery practices. He specializes in container orchestration, Amazon Connect, DevOps automation, and is a regular speaker at AWS events across Europe. Outside of work, Akash is passionate about knowledge-sharing and exploring the intersection of generative AI and cloud-native innovation.

Attila Dancso

Attila is a Senior Delivery Consultant with Amazon Web Services Professional Services, helping customers build customer experience solutions on Amazon Connect Customer. Alongside his contact center expertise, he has a background in software development and enterprise networking. Attila is always looking for innovative ways to extend product capabilities and deliver customer benefits.

 

Dheeraj Yadav

Dheeraj Yadav is a Terraform-certified Voice Engineer II at Booking.com, specializing in DevOps automation, Amazon Connect and platform engineering. His expertise includes infrastructure as code, Terraform automation, CI/CD, and the secure and reliable delivery of voice solutions. For this article, he contributed to designing the automation that manages and promotes Amazon Connect contact flows as code.

 

Otto Sanchez

Otto is a Senior Delivery Consultant with Amazon Web Services Professional Services, where he leads the delivery and architecture of large enterprise migrations from legacy contact center platforms to Amazon Connect Customer. With a background in enterprise collaboration solutions and software engineering, he focuses on removing undifferentiated heavy lifting for customers through systems that are efficient, durable and scalable. Outside of work, you can find him training for marathons, hiking, or deep in a book, often philosophy.