Skip to main content
Antal István

Thoughts7 min read

Rebuilding a B2B platform to last

Four years owning architecture and delivery: faster pricing, repeatable infrastructure and dependable releases for a 300,000-product B2B platform.

An old boxy computer sits on a small off-white shop counter at the lower left. From it, a blue conveyor belt carries a line of yellow and red parcels up to a large blue platform on thick legs, where tall green shelves hold far more boxes than the counter ever did.

When I joined Hostettler AG in 2020, its B2B shop was a ColdFusion application that looked like it belonged in the early 2000s. Customers could browse around 16 products at a time. The business wanted a modern shop where they could see more products, get more useful information and move through the catalogue quickly.

Hostettler is a Swiss commerce business, and the shop had to work with the systems it already ran. Product data, images, pricing and orders came with existing integrations and business rules. Replacing the website would not make those dependencies disappear. Much of the knowledge needed to work with them was still in people’s heads.

There was already a proof of concept when I arrived. I had initially been approached as a Node backend developer, but the interview made it clear that the work needed architectural ownership. I joined as architect and technical lead, responsible for designing the platform and leading a small remote team through the build. That responsibility would continue into production: when something went wrong, I was called.

Choosing what to keep from the proof of concept

The proof of concept used Next.js. In 2020, it was not reliable enough for our needs and brought complexity without enough benefit. This was a login-gated B2B shop; search-engine discovery did not justify operating a server-rendered frontend. I replaced it with a static React application. That gave us a frontend we could build and serve without operating an application server for it.

The application got its data from our GraphQL backend. We later pre-baked a small amount of navigation data at build time so the header did not have to wait for an initial API request. Serving the frontend through CloudFront from 2021 made it fast to deliver and straightforward to operate. We accepted the trade-offs of a client-rendered application because they fitted this product.

I also removed Elasticsearch from the early design. PostgreSQL met our search needs without a separate search system to deploy, monitor and keep in sync. We could add one later if the product needed it. Over the next four years, search grew inside PostgreSQL instead.

Building a usable catalogue

Simplifying our own application was only part of the work. The catalogue still had to come from the existing product system. We sourced products from Contentserv, working through inconsistent source data to build overlapping catalogues for multiple tenants. An ImageMagick pipeline converted source images, including PSD files, into assets suitable for the shop.

This became a multi-tenant platform with overlapping catalogues and around 300,000 products. The ingestion pipeline had to turn that source material into something the shop could use efficiently. A modern interface would not help much if every page still had to wait on the systems behind it.

Infrastructure we could reproduce and evolve

Moving the business from its on-premises ColdFusion shop to AWS meant building a new platform, rather than moving the old application onto a cloud server. I built and maintained the infrastructure, starting with CloudFormation and later migrating to CDK, using services including ECS and RDS. Jenkins handled our build and delivery pipelines.

Infrastructure as code made the setup reproducible and gave the team something concrete to maintain. Operating the platform did not depend on remembering a sequence of manual changes in a console.

The whole regression suite on every build

We also needed a way for a small team to keep changing the shop without manually checking the entire buying journey each time. The integrations and pricing rules made that a substantial job. I wanted those checks captured in the build, where every change would run through them.

We had strict linting and unit-testing standards. For end-to-end testing, I made the call to run the entire suite on every CI build, with its report attached in Jenkins. We did not select a smaller smoke-test suite for routine builds.

To make that practical, I provisioned powerful build runners through ECS and parallelised the tests. The machines were expensive while running, but the overall cost was small compared with repeated manual testing and uncertain releases. I monitored that spend; we paid for the runners while they worked rather than keeping them idle overnight.

Developers wrote end-to-end tests as part of their features. Each branch also had a frontend preview, so product could use and refine a change before it merged. Automated checks covered regressions while people concentrated on whether the feature did the right thing.

Master stayed releasable. After the initial migration release, we had no problematic releases during my engagement. That confidence was the return on the testing infrastructure and the extra work we put into each feature.

A new shop, still waiting for prices

We rolled out the replacement and moved the business off the old shop. Customers now had a modern interface with denser listings: 48 products on a default page, up from around 16. But after rollout, the legacy pricing service became the problem we needed to solve next.

A product could have eight variants. To show a starting price, we needed to know the prices of those variants. Showing more products meant asking the same old service to do much more work.

The service took roughly a second per request and supported no more than 20 concurrent requests. A richer catalogue meant hundreds of calls to a service that could barely keep up. We contained the problem with concurrency limits, caching, batching and deferred price loading. Those measures protected the upstream service, but they added maintenance work and left customers waiting. A large listing could take about a minute to finish loading with its prices.

I proposed calculating prices locally. The team responsible for the original service had a reasonable objection: B2B pricing is complicated, and duplicating its rules creates a risk of charging the wrong amount. Together with the project manager and product owner, I made the case over about two months. They carried the business case; I explained the technical approach and why the existing workarounds had reached their limit.

Four months of checking before switching

Once we had agreement, we ran our calculation alongside the original service for four months. Customers continued to see the upstream price while we compared the results to the cent and logged discrepancies for investigation. The new engine had to earn our confidence before it became authoritative.

Those comparisons also exposed rounding and data problems in the existing system. We investigated the differences rather than assuming that either implementation was automatically correct. After the switch, clear documentation of the pricing model and automated tests kept its behaviour verifiable.

Large product listings went from about a minute to under two seconds. We could remove much of the code that had been masking the slow service and support more complex B2B pricing. The cost was taking responsibility for that calculation ourselves, including its documentation and regression coverage.

Introducing Rust as the workload grew

With the shop in production, we continued evolving the platform. The frontend moved to Vite in 2022, keeping the same static application model. On the backend, larger processing jobs gave us a reason to reconsider the runtime for some tasks.

Later, when worker tasks needed to ingest a whole year’s order history from gigabyte-sized CSV files, I started introducing Rust. The Node implementation’s memory requirements were becoming a problem. Since the workers already ran as ECS tasks, integrating another runtime was manageable.

We then introduced GraphQL federation and added Rust services where they made sense, including authentication. Rust and TypeScript entities coexisted in a shared package, and the Node backend remained part of the system. One of the engineers I mentored went on to originate most of the Rust services; I owned their build, deployment and release infrastructure. The platform could evolve without requiring a wholesale rewrite.

What remained after I left

By the time I left in April 2024, typical daily orders through the platform were worth €150,000 or more, and the team could carry it forward. The infrastructure was defined in code, pricing rules were documented, and tests captured behaviour that had previously depended on people’s knowledge. Engineers who had started with frontend work were taking ownership of services and tooling. The platform remains in production after my departure.

One thing I would do differently is record the architectural rationale more consistently. We captured what the system did and how to run it, but too much of why we chose an approach lived in conversations and reviews. Today, AI-assisted, spec-driven development helps me preserve those decisions alongside the implementation.

The Hostettler case study on Avant Media includes the client’s account of our work together. For similar work, see how I work with clients.

Related reading