Skip to main content
Case Studies

CASE STUDY: DISTRIBUTION-CHANNEL MODERNIZATION AT GLOBAL HOTEL SCALE

Migrating a Live Hotel-Distribution Engine Off Oracle: Best Western's Distribution Platform

The stored procedures that decide whether a hotel room is available, at what rate, under what rules — for ~4,300 hotels in 100+ countries — were written in Oracle PL/SQL. My job on the Distribution team was to move them to PostgreSQL while bookings kept flowing through them.

John Pratt
John Pratt
February 1, 20269 min read
Migrating a Live Hotel-Distribution Engine Off Oracle: Best Western's Distribution Platform demo
$700K/yr
Oracle Licensing Cut
$4M
Loyalty Migration Saved
$43K/mo
Bookings Recovered
64M+
Loyalty Member Records
30M+
Annual Guests

Best Western distribution platform

Background

Best Western isn't a hotel chain in the way people picture one. Its parent, BWH Hotels, is a membership association of roughly 4,300 independently owned hotels across more than 100 countries, spanning eighteen brands from economy to luxury under three companies — Best Western, WorldHotels, and SureStay. The hotels are independent; what the association centrally operates is the machinery that gets those rooms sold: the reservation system, the connections to Global Distribution Systems and OTAs, and the loyalty program that ties a guest to the network rather than to one property.

That machinery is Distribution — the "DIST" team, where I worked as a software engineer from August 2025 to February 2026. Distribution is the layer that answers, thousands of times a minute, a deceptively simple question coming in from a travel agent's GDS terminal, an OTA, or brand.com: is this room available, at what rate, under what rules, and can this guest earn points on it?

Underneath that layer sat Oracle. Not the application — the database. The rate, availability, and inventory logic lived in Oracle PL/SQL stored procedures, and Oracle Database's per-core licensing is one of the most expensive line items an enterprise carries. The mandate was to get off it: translate the distribution engine to PostgreSQL and stop paying the licensing.

Worth noting the irony, because it's instructive rather than embarrassing: around the same era the company had publicly gone deeper on Oracle on the application side, becoming the first hotel group to integrate its central reservation system with Oracle's OPERA Cloud property-management system. Those are two different Oracle products with two different budgets — OPERA Cloud is the front-desk SaaS; Oracle Database is the engine — and a company can rationally adopt the first while fleeing the licensing on the second. The distribution backend was ours to control. So that's the one we moved.

Challenge

Translating a stored procedure sounds mechanical. Doing it to the beating heart of a live distribution system is not:

  • Oracle and PostgreSQL disagree about when things become true. This was the deepest problem and the least obvious. In Oracle, each procedure commits its own work and returns — call five procedures from a parent and all five have independently committed by the time control comes back. PostgreSQL doesn't work that way: nothing is durable until the whole transaction succeeds, at which point every table changes at once. That difference is invisible in a code diff and load-bearing in production. A translation that's line-for-line "correct" can still be wrong about the one thing a distribution system can't be wrong about — the order and atomicity of when inventory, rates, and restrictions become visible to the world.
  • A schema-conversion tool gets you a corpse, not a translation. The automated tooling left behind procedures that were half-commented-out and missing functionality outright — logic that existed nowhere else in the database and simply had to be rebuilt by hand. "The tool converted it" and "the procedure works" are separated by most of the actual work.
  • You can't diff your way to confidence; you have to prove equivalence. The only acceptable definition of "done" for a translated procedure is that it produces byte-for-byte the same effect on the database as the Oracle original, across valid inputs, invalid inputs, boundary dates, and null parameters. That's not a code review. It's a validation harness.
  • There is no maintenance window. Rooms sell 24 hours a day across every time zone the association operates in. The distribution engine is never idle, which means the migration runs alongside live traffic, and "we'll just take it down to cut over" was never on the table.
  • The distribution stack is legacy SOAP, and legacy is where the landmines hide. The Direct Connect service that fields OTA requests speaks SOAP/XML — the lingua franca of hotel distribution for twenty years. It also, in places, talked straight to downstream services and bypassed the Oracle layer entirely — which meant assumptions that held everywhere else in the system quietly didn't hold there.
  • An external payment change was about to break loyalty silently. The corporate-travel world is moving off stored 16-digit card numbers — Concur signaled it would stop supporting them by 2026, part of a broader GDS shift to anonymize card data in the booking record. That reverberates into loyalty in a way nobody would notice until it broke, for reasons I'll get to.

Solution

Translating the ARI engine, procedure by procedure

The core of the work was the rate/availability/inventory recalculation procedures — the ones that recompute what's bookable when a rate plan, restriction, or group block changes. My process was deliberately unglamorous: take what the conversion tool produced, uncomment it, run it, and let it break. Then go line by line against the Oracle original, translating PL/SQL to PL/pgSQL, making sure each statement actually corresponded to what Oracle did — not what it superficially resembled — and re-running until the PostgreSQL version moved the database exactly the way the source did. Where the tool had dropped functionality on the floor, I rebuilt it as new utility logic.

Solving the transaction-semantics gap

The commit-model difference couldn't be papered over — it needed a decision. I worked through it in sessions with the team, building small proofs-of-concept to characterize exactly how Oracle's per-procedure commit chain behaved versus PostgreSQL's all-or-nothing transaction, and where in the Direct Connect call graph that difference actually mattered versus where it was harmless. The point wasn't to force PostgreSQL to imitate Oracle everywhere — it was to know, per call path, whether the semantic difference changed an outcome, and to handle only the paths where it did. Getting that judgment wrong in either direction is a bug: mimic Oracle needlessly and you fight the database; ignore the difference where it matters and you corrupt inventory.

Making validation the actual deliverable

Because equivalence was the bar, the validation queries were the product as much as the translated code was. I wrote functional tests for the translated PostgreSQL procedures covering the cases that actually break stored procedures — valid group and cursor, invalid group, single-day and extended date ranges, null parameters, audit logging — so each translation had a repeatable proof that it matched Oracle before it went near production. That fed directly into raising the team's quality bar: I pushed for and enforced quality gates on the core SQL and Java codebases — functional tests on the translated SQL, unit tests on the Java, and a repair of the Java unit-test suite, which had been broken and was therefore protecting nothing.

The member-ID landmine — the fix nobody would have seen coming

This is the part I'm proudest of catching. Best Western Rewards member IDs exist in two forms: a 16-digit number, and a 10-digit number that Oracle converts to 16 by prefixing a fixed 600663. That conversion lived in Oracle. But the Java Direct Connect layer, when it allocated or released loyalty points, called the loyalty points service directly — Oracle was never in that path. So the conversion never ran there. As long as everything upstream sent 16-digit IDs, nobody noticed.

The Concur 2026 change was about to end that: affiliates would start sending 10-digit IDs, those would flow through the Java path untouched, and loyalty lookups would fail silently — members simply wouldn't get their points, with no error anyone would see until the complaints rolled in. I ran the analysis that surfaced the gap, then built the fix as a shared member-ID normalization library rather than a copy-pasted helper: normalize at the system boundary, in Java, before the external call, so the Java layer finally agreed with Oracle on what a member ID is. I made the case for the library over hardcoding explicitly — the JVM's JIT inlines these tiny static methods so there's no real performance cost, and a single canonical implementation means one bug fix instead of hunting duplicated logic across services. That library became the mechanism for migrating loyalty onto the new member-tracking system cleanly, across tens of millions of member records.

Killing the phantom-availability bug

The distribution SOAP API had a bug that was quietly costing bookings. The availability response (HotelAvail) and the rate-rules response (RateRules) checked a rate plan's inactive date differently — so a rate could still display as bookable in the rate-rules response after availability had already cut it off at its cutoff. A guest would see a rate, try to book it, and hit nothing — and because the booking flow set up a Stripe payment intent, that failure could leave a floating authorization behind. It surfaced most on brand-level promotions dumped across channels with hard cutoffs. I traced the discrepancy to how the inactive date was interpreted on each side and fixed the two responses to agree on exactly when a rate stops being shown.

Paving the road for everyone else

Two smaller things multiplied across the team. I onboarded the global dev team onto Claude Enterprise to speed up the migration — a translation-and-validation effort is exactly the kind of work an LLM accelerates when it's wired in safely. And I built a set of quality-of-life ops scripts used by 20+ engineers — an automated Markdown-to-MediaWiki processor for the internal wiki, Markdown-to-docx/PDF conversion for change-request paperwork, and more — all built to stay inside internal cyber guidelines. Small tools, but they removed friction from work that every engineer on the team did every week.

Results

The distribution engine moved off Oracle PL/SQL and onto validated PostgreSQL procedures, cutting roughly $700k/year in Oracle licensing and infrastructure while keeping bookings flowing for 30M+ annual guests. The loyalty member-ID library de-risked the migration to the new member-tracking system across 64M+ member records, contributing to a $4M saving and — more importantly — heading off a silent points-allocation failure before the Concur change could trigger it. The phantom-availability fix recovered bookings that had been leaking at roughly $43k/month. And the quality gates left the codebase with something it hadn't had: a repeatable way to prove a translated procedure was actually correct.

Why this matters for a business:

  • "Get off the expensive license" is a correctness project, not a cost project. The savings are real, but they're the easy part to say and the hard part to earn. What stands between you and them is proving that a reimplemented engine behaves identically — and that proof, not the translation, is where the risk lives.
  • The dangerous bugs live at the seams between systems. The member-ID failure wasn't in Oracle and wasn't in Java — it was in the assumption, true everywhere else, that Oracle sat in the path. Integration boundaries are where "always true" quietly becomes "usually true," and a change three companies away is what exposes it.
  • Databases have opinions, and migrations inherit them. Oracle and PostgreSQL disagree about when work becomes durable. Treating a migration as syntax translation misses exactly the class of bug that a distribution system can least afford — the kind that makes inventory briefly lie.
  • Displaying something you can't sell is worse than not displaying it. A rate that shows as bookable but isn't doesn't just lose the sale — it can strand a payment authorization and erode trust in every number you publish to a channel. Accuracy at the moment of display is the product.

Conclusion

Distribution is the unglamorous core of how a hotel network actually makes money — the layer that has to be right about availability, rates, rules, and loyalty every time, for thousands of independent hotels, in a legacy stack nobody gets to pause. Moving its engine off Oracle without dropping a booking, while catching a payment-industry change before it silently broke loyalty for tens of millions of members, is the kind of work that doesn't make a splash and absolutely cannot go wrong.

If you're carrying an expensive legacy database, a modernization you can't take downtime for, or integration seams where the real bugs hide, that's precisely the kind of problem I like. Book a call and let's dig in.

John Pratt

Founder, Pratt Solutions · Previously at Northern Trust, Duke Energy, Capital One

John Pratt

Built enterprise systems at Northern Trust, Duke Energy, and Capital One. Now freelancing and building tools that solve hard problems at scale.

© 2026 John Pratt. All rights reserved. | Privacy Policy
Pratt Solutions

Let's talk outcomes.

If you're ready to take your business to the next level, I'm ready to help.

I'll only use this to respond to your message. No newsletter, no marketing emails, no selling your info.