
The machine coding problems you get at three years often look like the ones you got as a fresher. The same list to render, the same search box, the same modal. What changes is not the prompt. It is that nobody tells you the requirements any more.
At entry level, more of the requirements may be stated explicitly: show these items, filter them by this input, handle the empty case. As your experience increases, interviewers tend to leave more of those decisions to you. Loading and error states, what happens when requests finish out of order, or what happens when the user navigates away mid-request may never appear in the brief. Noticing those cases without being prompted is increasingly part of what is being evaluated.
This guide covers machine coding interview questions for 3 years experience on that basis: one worked problem, and the complications a mid-level candidate is expected to raise on their own. If you are earlier than that, start with machine coding interview questions for freshers. For how the round itself is structured and timed, the machine coding round guide covers that ground.
It is worth being precise here, because the obvious answer is incomplete. The problem itself does not necessarily get harder.
A three-year candidate can absolutely be handed a plain todo list, and it is still a common warm-up. A fresher who is asked to build a search box can absolutely hit a race condition. Difficulty does not cleanly separate the levels, and a guide that promises harder problems at each rung is describing something that does not happen.
What increasingly changes is who is expected to raise the complication. At entry level the interviewer states a requirement and you implement it correctly. At mid-level the interviewer states less and watches what you add. Further up, at tech lead and principal level, the conversation moves off your implementation entirely and onto the API other teams will consume and reviewing code you did not write.
So the mid-level round is mostly the same surface with the scaffolding removed.
Take a prompt of the kind you might actually get: "Build a view that fetches a list of products and lets the user search them."
That is two sentences. Implemented literally, it is maybe twenty lines. Everything that makes it a real interview lives in what the sentence does not say, and the rest of this guide works through those.
// The literal reading of the prompt. It works, and it is not a passing answer.function ProductList() {const [query, setQuery] = useState('');const [products, setProducts] = useState([]);useEffect(() => {fetch(`/api/products?q=${query}`).then((res) => res.json()).then((data) => setProducts(data.items));}, [query]);return (<><label htmlFor="product-search">Search products</label><inputid="product-search"type="search"value={query}onChange={(e) => setQuery(e.target.value)}/><ul>{products.map((p) => (<li key={p.id}>{p.name}</li>))}</ul></>);}
The code above has no loading state, no error state, and no way to tell an empty result from a request that has not finished. The user sees an empty list in all three cases.
The instinct is to add isLoading alongside the data. That is where the mid-level answer improves on it: two independent booleans plus a data array can represent combinations that should never exist. You can be loading and errored at once, or finished with data and still showing a spinner, depending on the order your setters run.
Modelling the request status as one value prevents mutually exclusive request states such as loading and error from being active at the same time:
// One status at a time, so "loading AND error" cannot happen by accident.const [status, setStatus] = useState('idle'); // idle | loading | success | errorconst [products, setProducts] = useState([]);const [error, setError] = useState(null);
if (status === 'loading') return <Spinner />;if (status === 'error') return <ErrorMessage error={error} />;if (status === 'success' && products.length === 0) return <EmptyState query={query} />;return <ul>{/* ... */}</ul>;
This is the one that most often separates the levels, and it is worth tracing rather than asserting.
The user types "sh", then quickly types "oe" to make "shoe". Two requests go out. If the earlier request for "sh" takes longer than the later request for "shoe", the "shoe" response can arrive first and the stale "sh" response can arrive afterward. Your effect runs setProducts with the "sh" results, and the user is now looking at results for a query they have already finished typing. Nothing errored. The bug is purely in the ordering.
The fix is to make a stale response identify itself and do nothing:
useEffect(() => {const controller = new AbortController();const params = new URLSearchParams({ q: query });setStatus('loading');fetch(`/api/products?${params}`, { signal: controller.signal }).then((res) => {if (!res.ok) {throw new Error(`Request failed with status ${res.status}`);}return res.json();}).then((data) => {setProducts(data.items);setStatus('success');}).catch((err) => {if (err.name === 'AbortError') return; // superseded, not a failuresetError(err);setStatus('error');});return () => controller.abort();}, [query]);
AbortError check you will render an error for a request you cancelled on purpose. And this same cleanup runs on unmount, so navigating away also aborts the in-flight request.If you are not using fetch, the same idea applies with a captured flag: record whether this effect run is still the current one, and ignore the result if it is not.
The version above sends a request on every character. Mentioning that, and saying what you would do, is usually enough even if you do not implement it.
Debouncing the query means waiting until typing pauses before fetching:
const [query, setQuery] = useState('');const [debouncedQuery, setDebouncedQuery] = useState('');useEffect(() => {const id = setTimeout(() => setDebouncedQuery(query), 300);return () => clearTimeout(id);}, [query]);
debouncedQuery rather than query, so the input stays responsive while the network work does not.Worth noting: debouncing reduces the number of races but does not remove them. A user can still pause, trigger a request, then type again. Keep the abort from complication 2. A candidate who replaces cancellation with debouncing has made the bug rarer and harder to reproduce rather than fixing it, and an interviewer may well ask precisely that follow-up.
Decomposition is where mid-level candidates tend to err in both directions.
Splitting too late leaves one component holding fetching, filtering, pagination, and rendering, which is hard to talk about and harder to change under time pressure. Splitting too early produces a tree of small components passing props through layers that do not use them, which costs time you do not have and usually gets undone.
A workable rule under interview conditions: split when a piece has its own meaningful responsibility, state, or reuse, not merely because a file feels long. In this problem, a SearchInput that owns nothing is not obviously worth extracting, whereas separating the data fetching from the presentation is, because it lets you show the list with test data without touching the network.
Say the reasoning as you go. "I am keeping this in one component for now and would split the fetching out if we added a second consumer" is a better signal than silently doing either.
Knowing where the ceiling is helps you spend the time well.
You are generally not expected to design a component API for multiple consuming teams, argue a versioning strategy, or review someone else's implementation for production readiness. That is the level above, and it is covered in the tech leads and principal engineers guide.
You are also not usually expected to produce polished visual design, write a full test suite inside the time box, or handle deep accessibility beyond the basics such as labelling the input and not breaking keyboard navigation. Naming what you would add with more time covers this, and takes seconds.
isLoading next to the data without noticing the states it lets you represent that should be impossible.Will I be given a harder problem than a fresher? Not necessarily, and expecting that is a trap. You may well get a prompt that sounds simple. What differs is how much is specified and how much you are expected to surface yourself.
Should I use a data-fetching library if I would use one at work? Ask, rather than guessing. The round may be scoped to the underlying mechanics or to the finished result, and that changes what a library costs you. Asking takes five seconds and removes the guess. Being able to explain what the library does for you here - request lifecycle management, caching, deduplication, and, depending on the library, cancellation - is a good answer either way.
How much should I write versus explain? Working code that runs beats more code that does not. If time is short, implement the core path properly and describe the rest. Saying "I would add the error retry here, and cancel in-flight requests on unmount" while the working version renders is a strong close.
Is TypeScript expected? Follow the role. If the position is TypeScript and you are comfortable, use it. Do not adopt it in an interview to look thorough and then spend your time fighting types. For the type-modelling side of mid-level interviews specifically, TypeScript interview questions for 3 years experience covers that ground directly.
Build the problem above end to end, once, under a timer, and then deliberately break it: comment out the abort and add artificial latency so that an earlier request finishes after a later one. Watching stale results actually render makes the bug stick in a way reading about it does not, and it gives you a real answer when an interviewer asks whether you have hit that before.
After that, take two or three other everyday UI problems, a modal, a paginated table, a multi-step form, and for each write down only the complications the prompt would not mention. That list is the actual skill this round tests. The implementation is usually the easy part by three years; noticing what is missing is not.
Machine coding interview questions for 3 years experience are not a harder version of the fresher round. They are the same kind of problem with the requirements removed, and the evaluation is whether you fill them in yourself. Distinguishing loading from empty, preventing out-of-order responses from rendering, cleaning up on unmount, and being able to say why you split a component when you did are what a mid-level answer looks like. Say the tradeoffs out loud as you make them, because an interviewer can only assess the reasoning you actually voice.
A frontend-focused guide to machine coding round questions, including what is machine coding round, how to prepare for machine coding round interviews, and how to practice in React.
If you are a fresher preparing for a frontend or full-stack machine coding round, the good news is that the questions at your level are narrower than they look.