Back to Resume
PrivateDecember 2023· 7 weeks

AWS Connect Call Center Integration and AWS Infrastructure

A Multilingual Amazon Connect Contact Centre for a Fintech

RoleCloud and contact-centre engineer

A European fintech serving a largely Turkish-speaking customer base needed one phone number that greets a caller in German, Turkish, or English, verifies who they are before routing them, and hands agents the same case history whether the customer called or wrote in. I built the Amazon Connect contact centre end to end: the multilingual IVR, a phone-plus-passcode identity check, an email-to-task pipeline, an append-only case-audit ledger, and a streaming export that turns every contact into one predictable analytics record.

The problem

The client is a European fintech contact centre supporting a customer base that speaks German, Turkish, and English, with the banking side of the product run by a separate banking-as-a-service partner. Off-the-shelf contact-centre setups assume one language and one channel. This business needed three languages from a single entry point, an identity check that could tell a known customer from a stranger before an agent picked up, and a way to fold email support into the same routing and reporting as a phone call, so nothing had to be built twice. The naive fix, one phone number per language, was rejected early: three numbers to publish and pay for, three flows to keep synchronised, and reporting that has to union across entry points to answer a simple volume question.

Constraints

  • ·Three required languages (German, Turkish, English) from a single published number, chosen by the caller before anything else happens.
  • ·An 8-second Lambda ceiling inside the contact flow, so any synchronous check had to return well inside that window.
  • ·A private-subnet identity database (RDS inside a VPC), so the Lambda that queries it has no internet route without a NAT or VPC endpoint.
  • ·Case history had to be exportable outside the vendor platform for compliance review, independent of the contact-centre product's own retention.
  • ·No infrastructure-as-code for the IVR itself, contact flows are authored and exported from the vendor console by hand.
  • ·Small team delivering four independent, event-driven pipelines in about seven months.

Architecture

One managed contact-centre instance in a single AWS region, with four independent event-driven pipelines hanging off it: a synchronous identity check running inside the call flow, an asynchronous pipeline that turns inbound email into a Connect task, an append-only case-audit ledger, and a streaming transform that normalises every contact record into one shape. The four pipelines are deliberately independent and never import from each other.

  • Managed contact-centre platform: multilingual IVR, queues, routing profiles, agent workspace
  • Voice-auth Lambda plus a Postgres database in a private VPC subnet, returning a verdict only
  • Mail-to-task Lambda parsing inbound email and calling StartTaskContact
  • Case-audit Lambda with a DynamoDB mutex, appending to a per-case JSON ledger in S3
  • A streaming transform inside the Contact Trace Record delivery pipeline, normalising every record into S3
  • Console-authored pieces with no exportable source: speech synthesis, a post-call survey pipeline, and a BI dashboard reading the S3 sinks

The identity check runs synchronously, on the caller's clock, inside the flow. The other three pipelines are event-driven and never block a live contact.

How one request flows

  1. 01The call lands: the caller dials the single number and enters the main flow, with contact-flow logging turned on as the first block.
  2. 02Language is asked before anything else: German, Turkish, or English, stored as a contact attribute that rides with the contact from here on.
  3. 03The identity check runs: a Lambda checks the calling number against the customer table and the passcode against what that customer has set, returning a verdict and never customer data.
  4. 04The flow branches on the verdict: a match routes to the language-specific queue, no match plays a deliberately ambiguous rejection.
  5. 05An agent picks up with context already on screen: language and verification state, no separate handoff step.
  6. 06The contact streams out as a normalised Contact Trace Record, regardless of which channel it came in on.
  7. 07If it opened a support case, every event on that case is appended to a durable per-case ledger, safe under concurrent event delivery.

Engineering decisions

One number, language as a contact attribute

What. Language is asked once, at the top of the flow, and carried as a contact attribute into every prompt, queue, and the eventual analytics record.

Why. Keeps one number, one flow, and one report instead of three numbers to publish and three flows to keep in sync.

Tradeoff. A per-language DID was rejected, it multiplies telephony cost and splits reporting. Detecting language automatically from the caller's number was rejected too, a mobile number says nothing about preferred language, and account language is only known after authentication, which happens later in the flow.

A shallow identity check that gates routing, never money

What. The voice-auth check answers one question, phone number plus passcode matched against the customer table, and returns a verdict with no customer data attached.

Why. The check exists to route a call and give an agent context, never to authorise a transaction. Anything with money attached is re-verified by the agent in the banking system.

Tradeoff. Rate limiting, lockout after repeated failures, and an audit trail of failed attempts were all understood as needed and deliberately left out, on the basis that this check never authorises money movement on its own.

Two verification flags, never one boolean

What. Verification state is carried as two independent facts, whether the caller is a known number, and whether that customer has ever set a passcode, rather than one verified or unverified bit.

Why. A single boolean collapses 'known customer who never set a passcode' into the same state as an unknown caller, and that state needs its own handling on the agent side.

Tradeoff. Two flags cost more branching than one boolean, accepted because the collapsed state was a real gap, not a theoretical one.

Email becomes a Connect task, not a second helpdesk

What. Inbound mail is unwrapped from its SES and SNS envelope, parsed for sender and body, and turned into a Connect task carrying the same language and verification attributes a phone call would.

Why. Routing logic gets written once, for both channels, instead of a separate helpdesk with its own rules.

Tradeoff. Connect task attributes are a flat string map with no room for structured or binary data, so attachments are lifted out to object storage and passed as newline-joined links rather than as real attachments.

A lock around a read-modify-write with no atomic alternative

What. The case-audit ledger is one JSON document per case in object storage, appended to on every event. A conditional write against a small coordination table acts as a mutex, with exponential backoff on contention.

Why. Object storage offers no compare-and-swap for this access pattern, and agents creating a case and commenting on it within milliseconds meant concurrent writers were losing events without a lock.

Tradeoff. Moving the ledger into the coordination store entirely would have removed the race by construction, but the deliverable was a portable JSON document handed to a compliance team directly, and rewriting that consumer was rejected under deadline. A queue-level fix, grouping events per case so they serialise before reaching a Lambda, was the better long-run answer and was not taken, the queue was already wired as a standard queue.

Filling absent record fields from the schema, not leaving them out

What. A voice record, a task record, and a callback record do not share a shape, and task records carry values as name/value pairs rather than fields. The streaming transform flattens those pairs, then inserts a typed empty value for every field a given record does not have.

Why. So every record landing in storage carries the same key set, and downstream consumers can index any field without a guard.

Tradeoff. Absent and empty become indistinguishable to a downstream reader, and one merged schema is applied across every channel rather than the fully reconciled per-channel schemas that were derived by hand, that reconciliation was never finished into one canonical file.

Speech synthesised at call time, not recorded

What. The main flow plays synthesised speech against a small set of pre-recorded prompts, with the wording kept in one reviewable spreadsheet across all three languages.

Why. Adding or rewording a line becomes a flow edit instead of a studio session and a re-upload, which is what made three languages tractable for a small team.

Tradeoff. Costs control over pronunciation of product terms, which sits at the mercy of the voice engine rather than a human reading.

What changed along the way

  • The case-audit ledger went through three versions: a first cut with a plain read-then-write that lost events under concurrency, a second that added structured logging without fixing the race, and a third that added the coordination lock and backoff, the version that shipped.
  • Multilingual routing was built for voice from day one. Three flow-id variables exist for the email path too, but its language map was never finished and resolves everything to English, voice got all three languages, email got one.
  • A second identity Lambda, the one that actually compares the submitted passcode, turned out to be a separate function from the one that checks whether the customer record exists, a distinction found only by reading the flow graph, not the code.

Security and reliability

  • ·The voice-auth verdict never carries customer data back into the flow, and it gates routing and agent context only, never a financial action.
  • ·Every Lambda invoked from a flow has an explicit error branch, so a failing function degrades the IVR to a plain menu instead of dropping the call.
  • ·Lambda source reads configuration from the environment with no fallback, a required variable missing fails loudly at import rather than connecting to something unintended.
  • ·Known gap, flagged rather than silently accepted: passcode digits are collected without encryption enabled on the input block, so they can appear in flow logs in clear. The fix was identified (an encryption flag plus a signing key upload) and not applied before handover.
  • ·The case-audit lock has no expiry, so a Lambda that crashes while holding it locks that case's writes until someone intervenes, a known limitation at handover.

Metrics

3
languages served from one published number
measured
4
independent pipelines behind one contact centre
measured
93 / 6
blocks in the main IVR flow, of which 6 are Lambda calls
measured

What shipped

  • Delivered a working three-language IVR from a single number, with per-language queues and agent-connect prompts.
  • Folded inbound support email into the same queues, routing, and reporting as phone calls, through a dedicated mail-to-task pipeline.
  • Fixed a concurrency bug that was silently losing case events, replacing an unsafe read-modify-write with a locked, retried append.
  • Built a streaming transform that normalises voice, task, and callback records into one predictable shape for downstream analytics.
  • Left a documented, traced handover: every known defect and open question recorded with enough detail to act on without re-reading the whole system.

Lessons learned

  • ·In a contact-centre platform, most of the real logic lives in the flow configuration, not the Lambda source, reading only the code badly understates the system.
  • ·A lock without an expiry trades a lost-write bug for a stuck-forever bug under a rarer failure, a crash while holding the lock. Both need an answer, not just the common case.
  • ·Normalising a shape-unstable record at the point it is written is cheaper than pushing per-channel handling onto every consumer downstream, but it only holds if the schema is kept current.
  • ·Encryption on a captured input is opt-in on this platform, not default, so anything collecting a shared secret over a telephony keypad needs an explicit check, never an assumption.
  • ·Keeping independent pipelines from importing each other kept a rewrite of one, the case-audit locking fix, from ever risking the other three.

Tech stack

AI & retrieval
Amazon Polly (per-language speech synthesis, console-configured)
Backend
Python 3 Lambda handlersAmazon Connect contact flows (console-authored)API Gateway (customer lookup)
Data & state
PostgreSQL on RDS (VPC-private)DynamoDB (coordination lock)S3 (case ledger, attachments, exported records)
Ops & quality
Kinesis Data Firehose (record transform)EventBridge and SQS (case event delivery)SES and SNS (inbound mail)