Case study · Patterson Companies · 2025 to 2026

Rebuilding bill pay, then untangling a partner portal

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, then moved on to lead frontend architecture for a new B2B partner portal.

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.

The partner portal

Next I led frontend architecture for a B2B partner portal, where partners generate API secrets to integrate with Patterson's systems, and an admin site where employees manage partners and the customers assigned to them. The admin site replaced tracking that had lived in emails and documents.

The Vue code was split between a shared UI repo and two .NET site repos. I got the team's buy-in to move nearly all of it into the shared repo, routing included, so every screen could be demoed and tested in Storybook. The site repos now import the routes and pass them to a small launch function, an inversion-of-control pattern that keeps each repo to one job. The idea was mine; I used AI to help work out the details.

Shared UI repo feeding two site reposThe shared UI repo holds the components, routes, and Storybook demos. The partner portal and admin site repos, both .NET, import the routes and pass them to a launch function.Shared UI repoVue componentsRoutesStorybook demosPartner portal.NET, launch(routes)Admin site.NET, launch(routes)import

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.
  • Mocked up a partner sales dashboard in ECharts in about a day. Most people don't know what software they want until they see it, so something on screen beats a vague description.
  • 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.