50 questions across four roles — Junior, Mid, Senior, and Lead — with answers that draw from real projects (Helfa / Immigration Helper, Realworld DSAR, Fit Tracker) and from my path through Kyiv, Aschaffenburg, and the war in between.
Each question is colour-coded:
Technical a precise, code-grounded answer.
Story answer told from my own path — Ukraine, the war, restarting in Aschaffenburg, the pivot from architecture.
Tip what most candidates get wrong, and how I’d phrase it.
Watch out follow-up questions interviewers usually chain after this one.
Story “I’m a software engineering student at TH Aschaffenburg, but I came to code through a longer route than most. I grew up in Nigeria — my dad is a civil engineer, runs a construction firm, and when I was a kid he bought us a PC at a time when home computers were still scarce there. That made me the kid who fixed everyone’s laptop, and later the architecture student everyone came to for CAD and 3D-printer issues. I started architecture in Ukraine because of the family business, but the war made me leave mid-degree without my transcript. When I rebuilt my path in Germany — passed the FSP, picked Aschaffenburg for its course content and quieter setting — I treated it as a clean slate and finally went where my interest had always been: software. Today I build full-stack apps with Spring Boot and React; my pinned project Helfa is essentially turning the chaos I personally lived through into a tracked, sequenced product for other international residents.”
Story “Architecture was always my dad’s world — I went into it because his construction firm was the obvious next step. But the parts of arch school I loved most were already digital: parametric CAD, 3D-printing my models, the BIM tooling. After the war forced a restart, I asked myself what I’d pick if there were no expectation to inherit anything, and the honest answer was “the computer side of architecture, just without the architecture.” SDI is exactly that.”
Story “Two reasons, both concrete. First, after years in CAD I have a trained eye for spatial composition — I notice when something is half a pixel off and I care. Second, frontend is where the user’s pain becomes visible. On Helfa, I wrote backend code for visa state transitions, but it’s the React UI that actually decides whether a stressed-out international student understands what step they’re on. That feedback loop hooks me.”
Tip Always tie it to (a) something concrete in their stack or product, (b) something concrete from my projects, (c) a question I’d like to learn the answer to inside the team. Generic praise is detected instantly.
“Honestly, I’ve learned not to over-plan. Five years ago I was an architecture student in Kyiv. What I do plan: be a senior full-stack engineer who’s been the technical lead on at least one product I’m proud of, and a person whose junior teammates feel they grew faster because they sat near them.”
Technical JavaScript is single-threaded with a call stack and a task queue. The event loop’s job is one rule: when the call stack is empty, take the next task off the queue and run it. Microtasks (Promises, queueMicrotask, MutationObserver) drain fully between macrotasks (setTimeout, I/O, UI events). That’s why an unawaited promise chain can starve UI events if it never yields.
console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => console.log("c"));
console.log("d");
// Output: a, d, c, b
c print before b if both are 0ms?” — because microtasks drain before the next macrotask.== and ===?Technical === is strict equality, no coercion. == applies the abstract-equality algorithm, which coerces types — and that’s where the bugs live (0 == "", null == undefined, "" == false). I always use === except for the one canonical exception: x == null as a shorthand for “null or undefined,” which is explicit and idiomatic.
Technical A closure is a function bundled with the lexical scope it was defined in. In Helfa I use one for the auth token getter so the rest of the app never touches localStorage directly:
const tokenStore = (() => {
let cached: string | null = null;
return {
get: () => cached ?? (cached = localStorage.getItem("jwt")),
set: (t: string) => { cached = t; localStorage.setItem("jwt", t); },
clear: () => { cached = null; localStorage.removeItem("jwt"); },
};
})();
cached is private to the IIFE — three callers, one source of truth, no module-scoped let.
this refer to inside an arrow function?Technical Arrow functions don’t bind their own this; they inherit it from the enclosing lexical scope. That’s why arrows are the right choice inside map/filter callbacks and inside React class methods (rare now), and the wrong choice as object methods if you actually want this to point at the object.
Technical At the top of a scope, declarations are conceptually moved up. var is hoisted and initialized to undefined. let and const are hoisted but live in the temporal dead zone — accessing them before the declaration line throws ReferenceError. Function declarations are hoisted whole. Class declarations behave like let.
map, forEach, and reduce?Technical forEach is for side effects, returns nothing. map returns a new array of equal length. reduce folds to a single value of any shape. Rule of thumb: if you’re reaching for forEach to build an array, you wanted map; if you’re using map and not using the result, you wanted forEach.
Technical Destructuring binds names from the shape of an object/array. The pattern I lean on hardest is renaming with defaults in API handlers:
function fetchVisas({ status = "ALL", page = 0, size = 20 } = {}) {
// ...
}
null and undefined.Technical undefined = “the engine has not assigned a value here.” null = “a developer assigned the empty value on purpose.” In TypeScript I configure strictNullChecks and treat them differently: function parameters can be undefined; database fields that are explicitly empty are null.
Promise.all do, and when do you avoid it?Technical Runs promises concurrently and resolves with an array, but rejects fast on the first failure — the rest still run, you just don’t see them. When I want partial success (e.g. fetching the document checklist plus the office directory plus the user’s draft), I use Promise.allSettled and degrade gracefully.
Technical A type parameter — “same shape, different element type.” In Helfa I have a paginated response wrapper:
type Page<T> = {
content: T[];
page: number;
size: number;
total: number;
};
const visas: Page<VisaApplication> = await api.get("/api/v1/visas");
Technical An in-memory tree of plain JS objects representing what the UI should look like. On state change React diffs the new tree against the previous one and applies the minimal set of real DOM operations. The win is twofold: real DOM mutations are expensive, and your components stay declarative instead of imperative.
Technical Both run after render. useEffect fires after the browser paints — fine for fetching, subscriptions, logging. useLayoutEffect fires synchronously after DOM mutation but before paint — use it when you must measure layout and update state without a flicker (e.g. measuring a tooltip’s width to position it).
useMemo solve, and when is it a smell?Technical Memoises an expensive computation across renders. It’s a smell when used reflexively: every useMemo still pays a dependency-comparison cost, and most JS computations are cheap. I add it only after profiling shows a hot recompute, or when the value identity matters for downstream memo/effects.
Technical Controlled = state in React, value driven by props. Uncontrolled = state in the DOM, read via refs. I default to controlled because it composes with validation and conditional rendering. Uncontrolled is fine for big forms where re-rendering on every keystroke would be wasteful — react-hook-form is built around that idea.
Technical Two heuristics make the diff O(n): (1) elements of different types produce different trees — React unmounts the old subtree and mounts a new one. (2) the developer hints stable identity through key. Without keys, list reorderings cause unnecessary unmount/remount.
Technical Two rules. Call hooks only at the top level of a component or another hook — never inside conditions or loops. Call them only from React functions, not regular JS. The reason is that React identifies which hook is which by call order, not by name; an if would shift the order between renders and break the mapping.
Technical For real apps I use @tanstack/react-query: it handles caching, retries, deduping, and stale-while-revalidate by default. For quick prototypes a plain useEffect + axios is fine, but I never ship that — race conditions when components remount mid-fetch eat you alive.
const { data, isPending, error } = useQuery({
queryKey: ["visas", { status, page }],
queryFn: () => api.getVisas({ status, page }),
});
Technical Three layers. Server state → React Query (it’s a cache, not state). Persistent client state (auth, theme) → Zustand or Context with a reducer. Component-local state → useState. Redux only when the team is large enough that the boilerplate is paying for explicit traceability.
Technical A way to declaratively say “this subtree is pending; show this fallback.” The win is that loading states stop being scattered { isLoading ? ... } branches and become a layout primitive. Pairs with React.lazy for code-splitting and with frameworks that support streaming (Next.js).
Technical Five habits, in priority order: (1) semantic HTML — <button> not <div onClick>; (2) labels — every input has a real <label>; (3) keyboard — every interaction reachable via Tab/Enter/Esc; (4) focus management — when a modal opens, focus moves into it and traps until close; (5) screen-reader audit on the critical paths.
Technical Flexbox is one-dimensional — content flows along a single axis with optional wrap. Grid is two-dimensional — you define rows and columns at once. Rule of thumb: navigation, button rows, form rows → Flex. Page layouts, dashboard tiles, complex card grids → Grid. They compose.
Technical Each selector has a weight: inline (1,0,0,0) > id (0,1,0,0) > class/attr/pseudo-class (0,0,1,0) > element (0,0,0,1). Higher weight wins; ties go to source order. !important overrides everything in its layer. Modern escape hatches: cascade layers (@layer) for predictable third-party overrides.
Technical Every element is content + padding + border + margin. Default box-sizing: content-box means width counts only content; border-box includes padding+border. I always set * { box-sizing: border-box; } globally — it makes width math intuitive.
Technical Mobile-first. Start with the narrowest layout that works, then add breakpoints when the design needs them, not on a fixed schedule. Container queries (@container) for components that should respond to their parent, not the viewport.
Tip Honest answer: it’s the right default for me. The criticism — “ugly markup, can’t reuse styles” — falls apart once you actually compose components. Helfa, the portfolio site, the Realworld UI all use Tailwind. The escape hatch is real CSS in a stylesheet for animations or print rules.
Technical For Helfa: src/ is split into features/ (visa-application, document, office-directory, auth) and shared/ (UI primitives, API client, hooks, types). Each feature owns its own components, queries, and types — nothing reaches across features. The API client is a thin axios wrapper that injects JWT and surfaces typed responses generated from the OpenAPI spec. Routing is route-per-feature with lazy loading. Server state is React Query, client state is Zustand.
Technical Lift state up only as far as two siblings need to share it. Going higher creates re-render storms. The matrix I follow: server data → React Query (never duplicate). URL-shareable state → URL search params. Cross-feature persistent state → store. Component-local → useState. If a piece of state is hard to place, it’s usually because the component boundaries are wrong.
Technical Measure first. Lighthouse + the React Profiler. Then a fixed playbook: virtualize long lists (@tanstack/react-virtual), code-split routes, memoise the genuinely-expensive computations, defer heavy effects with startTransition, and ship a smaller bundle (audit with source-map-explorer). Premature optimisation is the failure mode.
Technical Three things matter more than the framework: (1) clear feature boundaries enforced by tooling (eslint-plugin-boundaries or a Nx workspace); (2) a small, opinionated component library so screens look consistent without anyone designing them; (3) PR review culture — small PRs, fast turnaround, conventions documented in code rather than wikis.
Technical By data freshness and SEO need. Marketing pages and docs → SSG. Authenticated app shells → CSR is fine. Content that changes frequently but tolerates seconds of staleness → ISR (or Next 16 Cache Components with cacheLife). Personalised pages where every render must reflect the request → SSR. I default to whichever the framework makes cheapest, then optimise hot paths.
Technical Treat the frontend as untrusted — every check happens server-side too. Then: never store secrets in JS, sanitise any HTML you render with dangerouslyInnerHTML, set a strict CSP, store JWTs in httpOnly cookies if XSS risk is high, audit dependencies (npm audit, Snyk), and don’t roll your own auth.
Story I’ve actually been the user the i18n is for — applying for visas in a language I didn’t speak fluently when I first arrived. My approach: extract every string from the start, not at the end. react-intl or next-intl with ICU message format because plurals and gendered words break naïve key-value translation. Build the locale switcher into the layout, not the user menu. And test with the longest language (German is brutal) so layouts don’t snap.
Technical Reproduce it deterministically first — that’s 80% of the work. Add structured logging at the suspect boundary, ship a debug build to the user reporting it, ask for a Sentry replay if available. If it’s race-condition-shaped, throttle the network in DevTools and try the worst sequence of clicks. Don’t fix anything until you can cause the bug on demand.
Story On Nova Schilda our group debated whether to model the network as an adjacency list or matrix. The matrix was easier to write but quadratic in memory; we had ~200 nodes and the algorithms we needed (BFS, Dijkstra, max-flow, bridge detection) all benefit from sparse representations. I built both with the same interface and benchmarked — the list won by >5× on the bigger inputs and the team agreed. The lesson I took: disagree with measurements, not with confidence.
Story On Helfa I had a JWT-refresh race condition: when the access token expired, two concurrent requests would both trigger refresh, the second would invalidate the first, and the user would be logged out mid-action. The fix was a single shared in-flight promise — if a refresh was already pending, every other call waited on the same promise instead of starting their own. Three lines of code, four hours to find.
Story Helfa, because it’s the only product I’ve built where I am literally the user. Every state in that visa state machine is a moment I lived through. That bias toward real friction shows up in small choices — append-only audit log so users can trust the timeline, real Ausländerbehörde directory instead of generic links, document checklists pulled from federal text instead of forum advice.
Story First version of the Talk-to-Anybody monorepo I tried to ship the API, mobile app, and shared schemas in one PR. The branch sat for two weeks and I lost the thread. Lesson: stage gates. The current version is structured around stages (scaffold → recording → analysis → purchases → dashboards) explicitly so I never have a 2,000-line PR sitting open again.
“Three buckets. Reversible & cheap — just do them. Reversible & expensive — timebox and try. Irreversible — stop, write down the decision, ask for a second opinion. Most fires are bucket one if you don’t panic.”
“Comment-by-comment, never a wall. Mark feedback as nit:, suggestion:, or blocker: so the author knows what’s required vs nice-to-have. Always read the whole PR before commenting; you don’t want to ask a question whose answer is on line 200.”
“Three habits. (1) Build small things in new tech as soon as it ships — Next 16 Cache Components, React 19 actions, Tailwind v4 — before deciding if I’d use them at work. (2) Read source code, not blog posts, for libraries I depend on heavily. (3) The GitHub release notes of React, Next, Spring Boot, and TypeScript are my actual newsfeed.”
Technical Tenant resolution at the edge — subdomain or path prefix — and inject a tenant id into every API call via an interceptor, never via parameters in business code. Theme tokens come from a tenant-config endpoint and are applied as CSS variables on the root, so a single bundle styles every tenant. Feature flags are tenant-scoped. Auth uses an OIDC provider that supports multi-tenant login (Auth0/Clerk).
Technical Three layers: tokens (colours, spacing, type), primitives (Button, Input, Card — un-opinionated), and patterns (FormField, EmptyState — opinionated about composition). Tokens live in CSS variables so dark mode and tenant theming are free. The primitive layer is what real designers and engineers fight over; lock it down with strict prop types and Storybook.
“Pair more than I review. A 30-minute pairing session prevents three days of wrong direction. I push them to write the smallest possible PR for any change so feedback is fast. And I ask them what their unblocking task is at the end of every standup; if I hear ‘I’m stuck’ twice in a row I sit with them.”
“Estimates are for ranges, not points. I split a feature into the smallest shippable increments, estimate each in days, and triple it for the unknowns. If the result is >3 weeks I split again — anything bigger is a bet, not a plan.”
Story “Because I have an unusual combination: trained spatial reasoning from architecture, lived experience as the kind of user civic-tech tries to serve, and the kind of resilience you only get from rebuilding a degree from scratch in your third country. On the technical side I’m comfortable across the stack — Spring Boot APIs, React UIs, Python algorithms, Godot games — and my pinned projects show I finish what I start. I won’t be the loudest engineer in the room. I will be the one who notices something is wrong in week two and quietly fixes it.”