Product design and frontend · Patient portal
How AdventHealth moved 250,000 patients onto a single health record across web, iOS and Android, without anyone losing access mid-migration.
The challenge
After a system-wide rebrand, AdventHealth needed to move its entire patient portal onto Epic, the largest electronic health record platform in the US, while three legacy systems (NextGen, Cerner, Athena) were still live.
The hard part was continuity. Patients had to keep reaching their records, bills and appointments throughout a multi-year, region by region cutover. I worked inside the AdventHealth team alongside the agency, owning design for the patient-facing flows where data from different systems had to feel like one product, and building the frontend components those flows ran on.
The core problem
Behind the portal, a single patient's records could be split across multiple record systems, with duplicate entries, overlapping appointments, and bills grouped differently in each one.
My job was to hide that complexity. The design had to present one clear timeline of visits, one consolidated set of bills, and one accurate record, even while the data underneath was being de-duplicated and migrated.
Design principle: the patient should never see the seams between systems.
Ideation and information architecture
Early on, the portal risked becoming a dumping ground, every feature the three legacy systems offered, bolted together. Before designing screens, I worked with product owners to map what patients actually came to do, and collapsed dozens of scattered entry points into four core jobs that became the backbone of the information architecture.
Wireframing
Before any visual design, I wireframed the highest-stakes flows to test hierarchy and confirm each screen worked with incomplete or still-syncing data. The goal was to settle structure cheaply, so high-fidelity design could focus on clarity and trust.
The approach
Because regions cut over to Epic at different times, the portal had to run on all systems at once during the transition. Every screen had to render correctly whether a patient's data came from Epic or a legacy system, and surface the right data as regions switched over.
Building it
A design principle like "never show the seams" only survives contact with production if the frontend enforces it. Alongside the design work I built the component layer these flows ran on, which meant the normalization problem landed on my side of the stack too.
Four record systems returned the same clinical concept in four shapes: different field names, different date formats, different identifiers for the same human being. Rather than let that leak into components, every source was mapped to one internal view model at the boundary, so a card rendering a visit never had to know where the visit came from.
That single decision is what made the four-job architecture hold. Components stayed dumb and reusable, and the messy part stayed in one testable place.
Reusable adaptive components backed a shared frontend system spanning 1,200 facilities. New features extended existing patterns instead of introducing new ones, which is the only way 40+ features ship without the product fragmenting.
Mid-migration data is rarely clean. Every component handled loading, partially synced, missing, duplicated and permission-restricted states explicitly, because an empty field in a health record reads as alarming rather than merely absent.
The same mental model had to hold on web, iOS and Android. Shared patterns and a single source of truth for layout rules kept the platforms from drifting as teams shipped in parallel.
Contrast, focus order, tap targets and keyboard behaviour were verified against WCAG 2.1 AA during handoff and design QA, on flows where a misread number is a safety problem.
// Every source system resolved to one shape before it reached a component,
// so a VisitCard never had to branch on where the visit came from.
type Source = "epic" | "cerner" | "athena" | "nextgen";
type Visit = {
id: string;
date: Date;
provider: { name: string; specialty?: string };
location: string;
source: Source; // kept, because provenance is trust
status: "confirmed" | "pending" | "syncing" | "unavailable";
};
export function toVisit(raw: unknown, source: Source): Visit | null {
const parsed = adapters[source].safeParse(raw);
if (!parsed.success) return null; // drop it, never half-render it
return { ...parsed.data, source };
}
// De-duplication ran on the normalized shape, not per source.
export const dedupe = (visits: Visit[]) =>
[...new Map(visits.map((v) => [visitKey(v), preferEpic(v)])).values()];
Production code from a healthcare platform is not mine to publish. This is the pattern reconstructed, to show the shape of the problem rather than the original source.
The work
I designed and built flows across the portal's core jobs: consolidated health data, care-team management, appointment confirm and reschedule, and medication refills.
Each had to handle the messy reality underneath, missing fields, data still syncing, records from different sources, with clear states so patients always knew what was current.
A closer look
"Get Care" was the portal's front door for action. The challenge was reducing a sprawling set of options, find a doctor, see a provider, start a video visit, schedule tests, into a few clear, confident entry points.
I designed these as a simple decision layer with strong visual hierarchy, then carried the same pattern across web and mobile for consistency.
Working within an established design system meant every new feature extended existing patterns rather than reinventing them.
Working at scale
This was a program, not a project. Designing inside it meant keeping patient-facing flows consistent while engineering rewired the data layer underneath, across a multi-year, region by region rollout.
Impact
The redesigned portal launched across AdventHealth's regions on a rolling, two-year timeline, giving 250,000 patients continuous access to records, billing and appointments while three legacy systems consolidated into Epic behind the scenes. Behavioural data from more than 50,000 sessions fed back into onboarding, which started from a 171 second baseline.
What I took away
The hardest problems here were invisible to the patient by design. Working at the scale of a live migration taught me that in regulated, high-stakes systems the best design decision is often the one that makes complexity disappear: clear states, consistent patterns, and a single source of truth the user can trust. It also taught me that a principle like "hide the seams" is a frontend commitment as much as a design one. Designing for data that is mid-migration is a discipline I now carry into every data-heavy product, including the agent interfaces I build today.
Public project metrics and scope are based on the published AdventHealth Epic case study. Screens shown are from project materials. The code sample is illustrative, written to show the pattern rather than reproduce proprietary source.