Case study · CTI and telephony

Replacing Avaya under a live contact centre

An agent desk and the middleware under it, built against a young contact-centre platform whose API was still growing while the project ran, where the schedule risk sat in another company’s backlog.

Client
Alfa-Bank, a large retail bank in Russia
Domain
CTI and telephony
My role
Agent desk and middleware
Period
2024–2025
Result
Several hundred agents, four months to beta

Context

Alfa-Bank was moving its contact centre off Avaya onto a domestic platform, several hundred agents in daily operation. I built two pieces of that: the browser agent desk in React, and the Node.js middleware that sits between the desk and the platform and owns the telephony session. The bank had other systems in the migration; this covers the part I wrote.

The new platform was young. It existed, it had customers, it published a REST and WebSocket API with a specification. It was also still filling in the parts of that API that a bank-scale contact centre needs, and it was doing that on its own schedule, in parallel with mine.

The problem

Replacing a switch is not a porting exercise. Avaya had decades of behaviour that nobody at the bank had to think about, because it had always worked. All of it had to reappear on a platform where some of it had never been written.

The published specification described what the platform intended to expose. What it actually did when you called it was a separate question, and the answer only exists on a running system.

Constraints

The contact centre stays up. Real customers were calling throughout. Agents could not be handed a desk that did less than the one they already had, so parity came before anything new.

The dependency was moving. Normally the platform under you is fixed and the risk is in your own code. Here part of my critical path lived in another company’s backlog, on a schedule I did not control and could not see clearly.

Browsers do not hold telephony state. A call is a state machine driven by events from the platform, and several events can arrive for one logical action, sometimes out of the order you would expect. Something had to hold the session, own that state, and hand the browser something simple to render.

Agents are measured in seconds. Call control that lags by a second is unusable in that job. It arrives written as a UX requirement and has to be engineered as a latency budget.

What I did

Built a test bench against the platform before building anything on it. Separate from the desk, separate from the middleware: a harness whose only purpose was to exercise the platform API and record what came back. This was the decision the project turned on. It is also the one I would repeat on any integration where the other side is new.

Wrote my side of every function completely, whether or not the platform answered yet. Implemented against the specification and exercised against the bench, sitting finished while the other side caught up. When the platform shipped a missing piece, the work waiting for it was already done and tested, so nothing had to start from scratch on a schedule that had already slipped.

Kept the platform as the authority on call state. The middleware holds a projection; the platform owns the truth. When they disagree, the projection is rebuilt. Any design where the middleware believes itself over the switch will eventually show an agent a call that is not there.

Kept the desk thin. React renders state and sends intent, and holds no telephony logic at all. Everything difficult stays in the middleware, where it can be tested without a browser.

What went wrong

Call transfer did not exist. Blind transfer and consultative transfer were both in the requirements, and both are table stakes in a bank contact centre. My tests on the bench showed neither one worked. Not a defect in how I was calling it: the platform had not implemented it.

That is a hard finding to act on, because the fix is not yours to make. What made it survivable was timing. The bench surfaced it during development rather than during agent training, and my side of the transfer flow was already written, which left the gap purely on the platform’s side. It closed considerably later than the schedule said it would.

The general lesson is duller than the incident. When you build on something young, your first deliverable should be a harness that tells you which promises are real, and it needs to exist before anyone commits to a date.

Reconnect forced a relogin. After a session drop, agents had to log in again rather than having state resynchronise underneath them. This one was mine, a defect rather than a design problem, and it was fixed.

Result

The number worth arguing about is not the four months. It is how long the transfer gap stayed open after the bench found it, and that time cost me nothing only because the work was already done and waiting.

Stack: Node.js · React · WebSocket · REST

Describe your problem in three sentences. I'll tell you honestly whether I can help.

If it isn't my kind of problem, I'll say so and point you somewhere better. Direct email works too: hi@realgeek.biz.