Interview prep ยท questions
Design and architecture interview questions
12 questions, from core to senior. Say your answer out loud first, then open the model answer and check it against the points a strong answer covers.
We have 40 minutes for a frontend system design question. How do you structure your answer and use the time?
Show the model answer
I follow five steps, called RADIO: Requirements, Architecture, Data model, Interface and Optimizations. First I ask questions and agree on the main features and the limits. Then I draw the main parts on the client and the server and say what each one does. Next I describe the data: the entities, their fields, and where each piece of state lives. Then I define the interface, which is the API between the parts: component props, and server calls with their request and response shapes. Last I go deep on optimizations: performance, accessibility, errors and edge cases. For 40 minutes I plan about 4 minutes for requirements, 8 for architecture, 4 for the data model and 8 for the interface. That leaves about 16 minutes for optimizations, because the deep questions come there. I watch the clock and move on when a step has used its time.
A strong answer covers
- Five steps in order: Requirements, Architecture, Data model, Interface, Optimizations
- Questions and an agreed scope come before any drawing
- Interface means the API between parts: component props and server calls
- A time split for 40 minutes, with the largest share for optimizations
- The clock is watched, and each step ends when its time is used
Likely follow-up
The interviewer gives you one sentence and no requirements. What do you ask first?
How do you choose between offset and cursor pagination, and between infinite scroll and numbered pages?
Show the model answer
I choose by how the data changes and how people use the list. Offset pagination sends a page number and a size. It is simple, the user can jump to any page, and the page number fits in a link. But when items are added or removed while the user reads, an item can appear twice or be skipped. A large offset is also slow in the database. Cursor pagination sends a pointer to the last item. It stays correct and fast when the data changes, but it cannot jump to page 50. On the screen, infinite scroll suits a feed that people browse without a goal. Numbered pages suit search results and tables, where people return to a place, share a link and need the footer. Infinite scroll is also harder for keyboard and screen reader users, so I often use a Load more button.
A strong answer covers
- Offset: simple, can jump to any page, fits in a link
- Offset problems: duplicates or skipped items when the data changes, and slow large offsets
- Cursor: correct and fast when the data changes, but no jump to a page number
- Infinite scroll for a feed; numbered pages for search results and tables
- Infinite scroll costs: the footer, returning to a place, keyboard and screen reader use
Likely follow-up
A user opens an item from an infinite list and presses Back. How do you return them to the same place?
How do you cache server data on the client, and how do you keep it fresh?
Show the model answer
I keep server data in a cache with one key per query, and I treat it as a copy that can be old. The key contains everything that changes the result: the resource, the id, the filters and the page. With stale-while-revalidate, the screen shows the cached data at once, the client fetches again in the background, and the new data replaces the old. After a mutation I invalidate the keys that the change affects, so they load again, or I write the server response directly into the cache. There are two cache shapes. A per-query cache stores each response under its key. It is simple, but one item can sit in two queries with different values. A normalized cache stores each entity once by id, so one update shows everywhere, but it needs more setup. For most REST applications I choose a per-query cache with careful invalidation.
A strong answer covers
- The key holds everything that changes the result: resource, id, filters, page
- Stale-while-revalidate: show cached data at once, fetch again in the background
- After a mutation: invalidate the affected keys, or write the response into the cache
- Per-query cache: simple, but one item can sit in two queries with different values
- Normalized cache: each entity stored once by id, with more setup
Likely follow-up
A user changes their name on the profile page. Which cached data must change, and how do you do it?
What is an optimistic update, and when would you not use one?
Show the model answer
An optimistic update changes the screen before the server answers, because we expect the request to succeed. I use it for small actions that almost always succeed and are easy to undo: a like, a checkbox, a rename. The steps are these. I cancel running fetches for that data, so they do not overwrite my change. I save the current value. I write the new value into the cache and send the request. If the request fails, I put the saved value back and show a clear message. When it ends, I fetch again, so the client matches the server. I do not use it when a wrong screen is costly or when the server decides the result: payments, bookings, stock, permissions. There I show a pending state and wait for the real answer.
A strong answer covers
- The screen changes before the server answers, because success is expected
- Good for small actions that are easy to undo: like, checkbox, rename
- The old value is saved and put back on failure, with a message
- A fetch after the request makes the client match the server
- Not for payments, bookings, stock or permissions: show pending and wait
Likely follow-up
The user clicks Like twice fast and the first request fails. What should the screen show?
Which states does every view that loads data need, and how do you design them?
Show the model answer
Every data view needs five states: loading, empty, error, partial and success. For loading I prefer a skeleton with the same shape and size as the real content. Then the page does not jump when the data arrives. A spinner is fine for a short action, such as saving a form. Empty means the request worked but there is nothing to show, so I explain why and offer the next action. Error means the request failed, so I say what happened in plain words and give a retry button. Partial means that some parts loaded and others failed, or that old data is shown while new data loads. I keep what works on the screen and mark the rest. I reserve space for images and late content to avoid layout shift. I also announce loading and errors to screen readers with a status or alert region.
A strong answer covers
- All five named: loading, empty, error, partial, success
- A skeleton with the final shape and size; a spinner only for short actions
- The empty state explains why and offers the next action
- The error state has a plain message and a retry; working parts stay visible
- Space is reserved to avoid layout shift, and changes are announced to screen readers
Likely follow-up
A page has five widgets and one of them fails to load. What does the user see?
How do you organize the code of a large frontend application?
Show the model answer
I organize it by feature or domain, not by file type. Each feature, for example checkout or search, has one folder with its components, hooks, API calls, state and tests together. A change to one feature then touches one folder, and it is clear who owns it. Each feature has a public API, usually an index file that exports what other code may use. Everything else is private. Imports go in one direction: the app layer uses features, and features use shared code. A feature never imports the internal files of another feature. Shared code has rules too. It holds only general things, such as UI components and helpers, with no business logic, and it never imports from a feature. I enforce these boundaries with a lint rule, so they do not depend on code review. Code moves to shared only when a second feature really needs it.
A strong answer covers
- Folders by feature or domain, each with its own components, hooks, API calls and tests
- A public API for each feature (an index file); everything else is private
- Imports go one way: the app uses features, and features use shared code
- Shared code is general, has no business logic and never imports a feature
- A lint rule enforces the boundaries
Likely follow-up
Two features need almost the same component. When do you move it to shared, and when do you keep two copies?
How do you decide where a piece of state should live in a React application?
Show the model answer
Every piece of state gets one owner, and everything else reads it or computes from it. I first ask what kind of state it is. Server data, such as a list of orders, belongs in the server cache, not in a global store. State that must survive a reload or be shared in a link belongs in the URL: filters, the page, the open tab. State that one component needs, such as an open menu or an input value, stays local. I lift it up only when another component needs it. What remains is global client state, such as the theme, and it is usually small. The main mistake is duplicated state: copying props or server data into local state, or storing a value that can be computed. Two copies drift apart, so I keep one and derive the rest during render.
A strong answer covers
- One owner for each piece of state; the rest read it or derive from it
- Server data lives in the server cache, not in a global store
- Filters, page and open tab live in the URL: they survive a reload and fit in a link
- Local state first; it is lifted up only when another component needs it
- No copies of props or server data in state; computed values are derived during render
Likely follow-up
A filter is in the URL, and the user is editing it in a form. Which one is the source of truth?
How would you design live updates in a web application, for example new messages or changing prices?
Show the model answer
I design four things: transport, reconnect, event order, and the merge into cached data. Polling is the simplest transport if a delay of a few seconds is acceptable. If only the server sends, I use Server-Sent Events, which run over normal HTTP and reconnect by themselves. If both sides send often, as in chat, I use a WebSocket. After a lost connection, the client reconnects with exponential backoff and a random delay, so all clients do not return together. Then it asks for what it missed, using the last event id, or it fetches the data again. Events can arrive twice or in the wrong order. So each event has an id and a version, and the client ignores duplicates and older versions. Last, I merge each event into the same cache that normal requests fill, so the screen has one source of truth.
A strong answer covers
- Transport by need: polling when a short delay is fine, SSE for server to client, WebSocket for both ways
- Reconnect with exponential backoff and a random delay
- After a reconnect: ask for missed events by the last event id, or fetch again
- Events carry an id and a version; duplicates and older versions are ignored
- Events update the same cache as normal requests: one source of truth
Likely follow-up
The user's laptop sleeps for an hour and then wakes up. What does your client do?
How do you design the API of a reusable component, for example a Select or a Modal?
Show the model answer
I start from how the component will be used, and I keep the props few. Every prop must be supported for years, so I add one only when a real case needs it. I avoid many boolean props, because they combine into states that nobody tested. One variant prop with named values is clearer. For flexible content I use composition: children, named slots, or compound parts such as Tabs with Tab and Panel. I avoid one large config object. For state I support both modes: controlled with value and onChange, and uncontrolled with defaultValue. Accessibility is built in and not left to each team: the correct element and roles, keyboard support, focus handling and a required label. Styles come from design tokens, so themes work. The root element accepts className, native props and a ref. Prop names follow the platform, such as disabled and onChange.
A strong answer covers
- Few props, each added for a real case; one variant prop in place of many booleans
- Composition: children, slots or compound parts, not one large config object
- Controlled (value, onChange) and uncontrolled (defaultValue) both work
- Accessibility inside the component: roles, keyboard, focus, label
- Design tokens for style; className, native props and a ref reach the root element
Likely follow-up
A team needs a variation that your component does not support. Do you add a prop?
What are micro-frontends, and when would you use them?
Show the model answer
Micro-frontends split one web application into parts that separate teams build, test and deploy on their own. A shell application loads the parts, by route or at run time, and joins them into one product. They solve an organization problem more than a technical one. They help when many teams work on one product and block each other with a shared release. The costs are real. Dependencies can load twice, so bundles grow, unless you share them and then manage versions. The user experience can become uneven between parts. Shared things such as login, routing and the design system need contracts. Local setup, testing and debugging get harder. For one team or a few teams I would not use them. A monorepo with clear module boundaries, an owner for each module and one build gives most of the benefit at a much lower cost.
A strong answer covers
- Definition: parts of one application that teams build and deploy independently
- The benefit is for the organization: many teams, independent releases
- Costs: duplicate dependencies, uneven UX, shared contracts, harder testing and debugging
- The simpler choice: a monorepo with clear module boundaries and owners
- A clear statement of when each choice fits
Likely follow-up
Two micro-frontends need different major versions of React. What are your options?
How would you move a large legacy frontend to a new framework without stopping feature work?
Show the model answer
I would migrate step by step and not rewrite everything at once. This is the strangler approach. The new application grows around the old one and takes over piece by piece, until the old one can be removed. Both frameworks run side by side in production during the move. Often I migrate by route: the router or a proxy sends some pages to the new application. Sometimes I migrate by component: a new component is mounted inside an old page. I start with a small, low-risk page to prove the setup. Each migrated part sits behind a feature flag, so I can release it to a few users and turn it off fast. Releases stay small. New features are built only in the new stack, so the team keeps shipping. Login, the API client and styles must work on both sides.
A strong answer covers
- No full rewrite: the strangler approach replaces the old application piece by piece
- Both frameworks run side by side in production during the move
- Migration by route or by component, starting with a small, low-risk page
- Feature flags and small releases, so each step can be turned off
- New features go only into the new stack, so delivery continues
Likely follow-up
Half of the pages are migrated, and the business asks why it takes so long. How do you show progress?
How do you know that your frontend works well for real users in production?
Show the model answer
I measure production, because tests alone are not enough. First, error tracking. Every uncaught error and unhandled promise rejection is reported with the release version, the route and the browser. Source maps make the stack trace readable, and the release shows which deploy started an error. Second, real-user monitoring. I collect the Core Web Vitals from real sessions: LCP, INP and CLS. I look at the 75th percentile by page and device, not at the average. Third, key flows such as sign-up and checkout. I track their steps and set alerts for when the success rate drops or errors rise. Fourth, feature flags. A risky change goes to a small percent of users first, so I can compare and turn it off without a deploy. If we use session replay, inputs and personal data are masked by default, and we follow the consent rules.
A strong answer covers
- Errors are reported with the release version and source maps, so the bad deploy is found
- Real-user Core Web Vitals (LCP loading, INP input response, CLS layout shift) at the 75th percentile
- Alerts on key flows such as sign-up and checkout: success rate and error rate
- Feature flags: a small percent of users first, compare, turn off without a deploy
- Session replay masks inputs and personal data and follows the consent rules
Likely follow-up
Lab scores are good, but real users say the page is slow. How do you investigate?