[{"data":1,"prerenderedAt":394},["ShallowReactive",2],{"page:work":3,"work:all":23},{"id":4,"cta":5,"extension":11,"header":12,"meta":16,"sections":17,"seo":18,"slug":20,"stem":21,"__hash__":22},"pages\u002Fpages\u002Fwork.yml",{"title":6,"note":7,"button":8},"Have something like this? Tell us about it.","The first call is free, takes 30 minutes and commits you to nothing.",{"label":9,"to":10},"Book a call","\u002Fcontact","yml",{"eyebrow":13,"title":14,"intro":15},"Work","What we built, and how it went.","What the problem was, what we did about it, and what changed. No stats we can't stand behind.",{},[],{"title":13,"description":19},"Case studies from Software Sage. What the problem was, what we did about it, and what changed.","work","pages\u002Fwork","XOdPVFwlpZXyQzEhcTgVUq8MG4tnbg5QxGk5JkYwSl8",[24,202,307],{"id":25,"title":26,"body":27,"client":113,"compare":114,"country":122,"description":106,"extension":123,"glance":124,"industry":128,"logo":129,"meta":134,"navigation":135,"order":136,"path":137,"people":138,"peopleNote":162,"period":163,"published":135,"quote":164,"seo":165,"service":166,"stack":167,"stem":174,"summary":175,"team":176,"timeline":177,"__hash__":201},"work\u002Fwork\u002Fvialuxury.md","Moving a live booking platform from Ruby to .NET without stopping it",{"type":28,"value":29,"toc":105},"minimark",[30,35,39,42,46,49,52,55,59,66,72,78,84,88,95],[31,32,34],"h2",{"id":33},"the-problem","The problem",[36,37,38],"p",{},"Vialuxury sells hotel stays with the extras included: dinners, wellness, experiences. Their platform was built in Ruby on Rails, and it worked. It was quick to change and the team knew it well.",[36,40,41],{},"Two things had become a problem. It had become expensive, and at busy times it fell behind on hotel availability updates, so the prices on the site could lag behind what hotels had released. Rewriting it in one go was not an option. Bookings come in every hour of the day, and years of business rules lived in the code, some of them in files rather than the database.",[31,43,45],{"id":44},"what-we-did","What we did",[36,47,48],{},"We built a new .NET core next to the Ruby one and moved the platform over one area at a time. Both systems run against the same Postgres database. Rails still owns the schema and .NET maps onto the existing tables, so switching an area over is a routing change that can be undone.",[36,50,51],{},"We started with accounts and passwordless sign-in, then finance reporting and invoicing, hotel availability, cancellations and refunds, and the sync of all content from the CMS. Checkout and payments go last.",[36,53,54],{},"Nothing took over until it matched Ruby on real data. A parity harness runs the Ruby and .NET versions side by side against the same CMS content and compares every row they write. Checkout runs in shadow mode: every live order is also calculated by .NET and compared with what Ruby charged.",[31,56,58],{"id":57},"what-was-hard","What was hard",[36,60,61,65],{},[62,63,64],"strong",{},"Rules outside the database."," Email templates, discount rules and a partner list lived in Ruby files and constants. They had to be found by reading code, not data, and one list changed on the Ruby side after we had ported it.",[36,67,68,71],{},[62,69,70],{},"A type system that let things slide."," Hotels and packages both have a status, but 0 means draft for a hotel and published for a package. Ruby read some whole numbers as decimals and turned missing amounts into zero without complaint. The parity runs caught these one at a time, and a few differences turned out to be Ruby bugs that the .NET version now fixes.",[36,73,74,77],{},[62,75,76],{},"Two systems, one database."," Ruby's background jobs still run against the same tables. An old nightly job could overwrite a field the .NET side had just set, and removing a Ruby job could quietly stop something that depended on it. Every change started with finding who else writes to that table.",[36,79,80,83],{},[62,81,82],{},"A rollback."," The first switch of cancellations to .NET was undone the same day: under production load, background jobs on one server held locks the others were waiting for. It went live again four days later.",[31,85,87],{"id":86},"what-changed","What changed",[89,90,92],"case-highlight",{"label":91},"At peak times",[36,93,94],{},"When many hotels update at once, availability no longer falls behind. The slowest updates now land in seconds, where on Ruby they could take minutes. Customers see the prices hotels have released, even at the busiest moments.",[96,97,98,102],"ul",{},[99,100,101],"li",{},"Hiring is easier. There are more .NET developers to choose from than Ruby ones.",[99,103,104],{},"When Ruby is switched off in October 2026, Vialuxury runs one stack instead of two. Until then it stays up as a fallback, which the business asked for.",{"title":106,"searchDepth":107,"depth":107,"links":108},"",2,[109,110,111,112],{"id":33,"depth":107,"text":34},{"id":44,"depth":107,"text":45},{"id":57,"depth":107,"text":58},{"id":86,"depth":107,"text":87},"Vialuxury",{"before":115,"after":116,"beforeAlt":117,"afterAlt":118,"width":119,"height":120,"caption":121},"\u002Fwork\u002Fvialuxury-home-before.webp","\u002Fwork\u002Fvialuxury-home-after.webp","Vialuxury home page before the redesign","Vialuxury home page after the redesign",1288,936,"The home page before and after the redesign Ilhan built. The new version is live as an A\u002FB test.","Netherlands","md",{"problem":125,"approach":126,"now":127},"A Ruby platform that worked, but had become expensive and fell behind on hotel availability at busy times.","Built a .NET core next to it on the same database and moved one area at a time, each proven against Ruby first.","Accounts, finance, availability, cancellations and content run on .NET. Checkout runs in shadow mode. Ruby is switched off in October 2026.","Luxury travel",{"name":113,"src":130,"srcDark":131,"height":132,"ratio":133},"\u002Flogos\u002Fvialuxury.svg","\u002Flogos\u002Fvialuxury-light.svg",72,1.2633,{},true,1,"\u002Fwork\u002Fvialuxury",[139,144,149,154],{"name":140,"role":141,"image":142,"link":143},"Aleksa Kuzman","Architecture of the .NET core, content sync, search indexing, redirects","\u002Fteam\u002Faleksa.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Faleksa-kuzman-653998190\u002F",{"name":145,"role":146,"image":147,"link":148},"Boris Radevic","Accounts, finance, availability, cancellations, checkout and payments","\u002Fteam\u002Fboris.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fboris-radevic-46337b2ba\u002F",{"name":150,"role":151,"image":152,"link":153},"Ilija Racic","Tracing, logging and alerting; content sync fixes","\u002Fteam\u002Filija-2.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Filija-racic\u002F",{"name":155,"role":156,"image":157,"link":158,"website":159},"Ilhan Kalač","The redesigned front end: search, offer pages, checkout, account and home","\u002Fteam\u002Filhan.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Filhankalac\u002F",{"label":160,"to":161},"ilhan.io","https:\u002F\u002Filhan.io\u002F","Search, the product pages and the home page API were built by another team working on the same platform.","February to August 2026",null,{"title":26,"description":106},"Modernisation and migration",[168,169,170,171,172,173],".NET","PostgreSQL","Wolverine","Elasticsearch","Redis","Nuxt","work\u002Fvialuxury","A hotel booking platform moved from Ruby to .NET one area at a time, with both systems running on the same database. Each piece took over only after it matched the old one on real data.","4 engineers",[178,181,184,187,190,193,196,198],{"date":179,"text":180},"Feb 2026","New .NET core running next to Ruby on the same database",{"date":182,"text":183},"Mar 2026","Accounts and passwordless sign-in",{"date":185,"text":186},"Apr–May 2026","Finance: booking and payout overviews, invoicing",{"date":188,"text":189},"Jun–Jul 2026","Hotel availability processing",{"date":191,"text":192},"Jul 2026","Cancellations and refunds, after one rollback",{"date":194,"text":195},"Aug 2026","Content sync from the CMS, and redirects",{"date":194,"text":197},"Checkout, pricing and price checks running in shadow mode",{"date":199,"text":200,"upcoming":135},"Oct 2026","Ruby switched off (scheduled)","AdZz1kmRPzx6Q3hNedaUJebJY1gEOINN3639m-pOIUA",{"id":203,"title":204,"body":205,"client":262,"compare":164,"country":263,"description":106,"extension":123,"glance":264,"industry":268,"logo":269,"meta":274,"navigation":135,"order":107,"path":275,"people":276,"peopleNote":164,"period":279,"published":135,"quote":164,"seo":280,"service":281,"stack":282,"stem":288,"summary":289,"team":290,"timeline":291,"__hash__":306},"work\u002Fwork\u002Fcorning.md","One home for test data from Corning's outside manufacturers",{"type":28,"value":206,"toc":256},[207,209,212,215,217,220,223,226,229,231,237,243,249,253],[31,208,34],{"id":33},[36,210,211],{},"Corning's optical business has many of its products made by outside manufacturers. Each factory tests what it builds and sent the results to Corning as CSV, JSON or Excel files, dropped into shared folders. Every factory had its own layout, nothing was read automatically, and there was no way to look across factories, products or units.",[36,213,214],{},"At the same time a new test lab was being set up to check samples from those factories, and it had no system for taking work requests.",[31,216,45],{"id":44},[36,218,219],{},"Eagle Eye II is Corning's platform for this data, built on Azure. Our part was getting the data in, and shipping the whole thing safely.",[36,221,222],{},"Files arrive by SFTP, through a storage drop, a watched folder or an API. Each one is stored as received, then read by a parser that fits the factory's format: CSV, JSON, Excel, the original Eagle Eye format so older test stations keep working, and spreadsheet layouts described in configuration rather than code.",[36,224,225],{},"A new product used to need its test plan and specification set up by hand before its first file could be read. Now a file that names an unknown product, plan or measurement registers it on the spot, and the pass\u002Ffail limits travel with each measurement. For unfamiliar spreadsheets, the system suggests a column mapping using AI.",[36,227,228],{},"Engineers browse from a unit to its measurements to a single test step. Releases are built once and promoted from development to staging through a gated pipeline. For the lab we built a request flow: a step-by-step form with drafts and attachments, request numbers, email updates, and a queue for the lab manager.",[31,230,58],{"id":57},[36,232,233,236],{},[62,234,235],{},"No two factories send the same spreadsheet."," Some are turned on their side. Some have a different number of test steps per unit. One fiber-array workbook holds 254 measurements per unit in merged header blocks spread across three sheets.",[36,238,239,242],{},[62,240,241],{},"A measurement and its limits were stored as one thing."," That's why every new product needed setting up first. Separating what a measurement is from what counts as a pass made automatic registration possible.",[36,244,245,248],{},[62,246,247],{},"Not every gap is a failure."," A measurement with no limits to judge it against must not show up as a fail, and a missing measurement is reported rather than holding back the whole file. A factory re-sending the same file is recognised from its contents and ignored.",[31,250,252],{"id":251},"whats-next","What's next",[36,254,255],{},"Production, then the outcomes we can write up with Corning.",{"title":106,"searchDepth":107,"depth":107,"links":257},[258,259,260,261],{"id":33,"depth":107,"text":34},{"id":44,"depth":107,"text":45},{"id":57,"depth":107,"text":58},{"id":251,"depth":107,"text":252},"Corning","United States",{"problem":265,"approach":266,"now":267},"Test data arrived as each factory's own spreadsheets in shared folders, with no automation and no single view. The new test lab had no way to take requests.","Ingestion that reads every factory's format and registers new products and specs by itself, plus a build-once pipeline from development to staging.","Running on staging. The lab request module is built and waiting to go live.","Manufacturing",{"name":262,"src":270,"srcDark":271,"height":272,"ratio":273},"\u002Flogos\u002Fcorning.svg","\u002Flogos\u002Fcorning-light.svg",34,6.8157,{},"\u002Fwork\u002Fcorning",[277],{"name":140,"role":278,"image":142,"link":143},"Ingestion, automatic product and spec registration, delivery pipeline, lab requests","From August 2026",{"title":204,"description":106},"Product development",[168,283,284,285,286,287],"Angular","Azure SQL","Azure Blob Storage","Cosmos DB","GitHub Actions","work\u002Fcorning","Factories making Corning's optical products sent their test results as spreadsheets in their own formats. We built the part that turns any of those files into one record of every unit and every measurement, and the pipeline that ships it.","In partnership with Cramick IT",[292,295,297,299,301,303],{"date":293,"text":294},"Jun 2026","Eagle Eye II project starts",{"date":194,"text":296},"Pipeline: build once, promote from development to staging",{"date":194,"text":298},"New products, plans and specs register themselves from incoming files",{"date":194,"text":300},"Parsers for rotated, variable and multi-sheet spreadsheets; the original Eagle Eye format accepted",{"date":199,"text":302},"Lab request module built",{"date":304,"text":305,"upcoming":135},"Next","Production","I2Yj6RNSBBQrtbuMWMcNHHxfek_GlLrp1YNBxn0ExVY",{"id":308,"title":309,"body":310,"client":366,"compare":164,"country":367,"description":106,"extension":123,"glance":368,"industry":372,"logo":373,"meta":378,"navigation":135,"order":379,"path":380,"people":381,"peopleNote":164,"period":384,"published":135,"quote":164,"seo":385,"service":386,"stack":387,"stem":390,"summary":391,"team":392,"timeline":164,"__hash__":393},"work\u002Fwork\u002Fb2bee.md","The platform under an AI agent that takes wholesale orders in chat",{"type":28,"value":311,"toc":360},[312,314,317,320,322,325,328,331,335,338,344,347,351,354,357],[31,313,34],{"id":33},[36,315,316],{},"B2Bee runs ordering for wholesale distributors. Its agent takes orders in chat: customers write over WhatsApp, Viber, SMS or web chat, in text, voice notes or photos, and the agent builds a basket from that distributor's own assortment, in ten languages.",[36,318,319],{},"Every distributor brings its own customers, products and prices, so the platform under the agent has to keep them apart, onboard a new one without hand work, and release changes without putting live ordering at risk.",[31,321,45],{"id":44},[36,323,324],{},"We rebuilt the platform from eleven written decision records. It runs on Azure Container Apps, every message goes through Service Bus, a control plane works out which distributor each message belongs to, and every distributor has its own database.",[36,326,327],{},"Onboarding a distributor became a 15-step script that can be dry-run first or resumed from any step. Each release promotes the same image from dev to staging to production, behind an approval.",[36,329,330],{},"We hardened the campaign engine distributors use to reach their customers first, with abandoned baskets, reorder reminders and broadcasts over WhatsApp, Viber and SMS. It gained throttling, per-client quotas and conditions that react to whether the customer has replied.",[31,332,334],{"id":333},"measuring-search-before-changing-it","Measuring search before changing it",[36,336,337],{},"Before changing product search, we measured it on 300 real purchases. Rules alone placed about half the order lines. A model that saw only the lines the rules could not place took that to most of them, at the same precision.",[89,339,341],{"label":340},"On 300 real purchases",[36,342,343],{},"Rules alone placed 48% of order lines at 99.3% precision. Adding a model for the lines the rules could not place raised that to about 86% at 99.2%, for €1.37 per thousand lines. On vague requests, the model added the wrong product five times as often as the rules.",[36,345,346],{},"We report the last figure next to the headline. It is where the model needs a person or a rule to check it.",[31,348,350],{"id":349},"designed-not-yet-built","Designed, not yet built",[36,352,353],{},"We have also designed an agent that starts the sale itself. An event engine raises signals per customer: a reorder that is late, an overdue invoice, an upsell seen among similar customers, a customer drifting away.",[36,355,356],{},"Each signal climbs five levels of autonomy: shadow mode, suggestions to a sales rep, messages a person approves, autonomy within limits, and finally self-optimisation. It moves up only on measured outcomes that a named person approves, and drops back on its own when a limit is crossed.",[36,358,359],{},"Every automated message passes one outbound gate that checks caps, quiet hours, consent, holdout groups and whether the signal is still fresh. The model writes the message, but policy decides whether it is sent.",{"title":106,"searchDepth":107,"depth":107,"links":361},[362,363,364,365],{"id":33,"depth":107,"text":34},{"id":44,"depth":107,"text":45},{"id":333,"depth":107,"text":334},{"id":349,"depth":107,"text":350},"B2Bee","Serbia",{"problem":369,"approach":370,"now":371},"An agent that takes orders in chat for many distributors, each with its own customers, assortment and data.","Rebuilt the platform from eleven written decision records, hardened the campaign engine, and measured search on real purchases before changing it.","The platform, campaigns and search are rebuilt. An agent that starts the sale itself is designed, not yet built.","B2B wholesale software",{"name":366,"src":374,"srcDark":375,"height":376,"ratio":377},"\u002Flogos\u002Fb2bee.webp","\u002Flogos\u002Fb2bee-light.webp",40,3.33,{},3,"\u002Fwork\u002Fb2bee",[382],{"name":145,"role":383,"image":147,"link":148},"Architecture of the AI layer and the platform under it","February to October 2026",{"title":309,"description":106},"AI and platform architecture",[388,389],"Azure Container Apps","Azure Service Bus","work\u002Fb2bee","A B2B commerce platform runs ordering for wholesale distributors through an AI agent in chat. We were the architects of that AI layer and rebuilt the platform under it, then measured product search on real purchases before changing it.","1 architect","Z1sIstTmZ4r_lplrXVkP1l3Uy0ZWO_wSiQqzgIm755Q",1793174665534]