Case study · Patterson Companies · 2025 to 2026

Rebuilding a revenue-critical bill-pay frontend

Patterson's dental and veterinary sites handle billions in payment revenue. Their bill-pay section was AngularJS code written six to eight years earlier, desktop-only and not responsive. I joined a small team to rebuild it.

Role
Principal Software Engineer (contractor)
Team
For bill pay, me, a senior frontend developer with a decade of tenure at the company who mentored me, and a junior developer
Stack
Vue, TypeScript, TanStack, Storybook, .NET/C#, Azure DevOps

The bill-pay rewrite

We rebuilt it in Vue with the Composition API. The company's shared component library was still on Vue 2, so we used the latest Vue 2, whose syntax is close to Vue 3, to keep the eventual upgrade easy. We built every component in Storybook, made them responsive, and kept all text out of the code so the UI follows the user's selected locale.

Card numbers never touched our code: a front-end tokenizer from the payments vendor gave us a token instead. The project grew to include payment history, canceling a pending payment, and a solid layer of unit tests.

Two sticky elements that didn't share a parent

On the pay-by-invoice page, the designer wanted the selected-payments summary and the invoice table header to stay in view no matter how far you scrolled. The two were cousins in the DOM: same grandparent, different parents. I said in the design review I wasn't sure it was practical, was told it was worth trying, and found an approach that worked in Storybook.

It broke on the real page, where the surrounding markup changed the offsets it depended on. The fix was a ResizeObserver that measures the sticky headers as they change size, so each one's position stays correct no matter what's around it.

Two sticky elements with different parentsA shared grandparent contains two parents. One holds the payment summary, the other the invoice table header. A ResizeObserver measures both sticky elements and sets their offsets.Shared grandparentParent ASummary (sticky)Parent BTable header (sticky)Rows scroll belowResizeObserversets offsets

One payment-method form, three places

Adding a payment method had to work on the payment methods screen, inside pay by amount, and in the review step of pay by invoice. Instead of copying the component, we used Vue's provide and inject: each part of the app provides its own state, and the shared component injects it. This sharpened my sense of when state belongs in a global store like Pinia and when it belongs in dependency injection.

One component, three providersThe payment methods page, pay by amount, and the pay by invoice review step each provide their own state to a single shared add-payment-method component, which injects it.Methods pageprovides statePay by amountprovides stateReview stepprovides stateAdd payment methodshared, injects state

What I'd do differently

The design packed more columns into the invoice table than would fit. We ended up with either heavily truncated text in a column or horizontal scrolling inside a table that also scrolled and paginated vertically. This was never fully resolved during my time there. I wish I'd pushed back harder at the start. The real fix was fewer columns, or user research into which ones people actually needed.

Along the way

  • Built a reusable Vue component library for embedded Power BI reports, and locale handling for English (U.S.), English (Canada), and French (Canada), and rolled it out to both e-commerce sites.
  • Moved the organization's front-end packages from a private npm registry to JFrog Artifactory, using a script I had AI write to make this effiecient for work across a couple dozen repos.