This page is for developers who build features from the database to the screen and want to show they can connect the layers. Full stack rounds usually open with your projects and why you work across the stack, then test how a request travels, REST versus GraphQL, login flows, database design, caching, CORS and the big web security bugs. Stronger rounds add a slow page to debug, a risky release and a disagreement with design or product. Each question shows what the interviewer is really checking, a shape for your answer and a short answer to say out loud. Swap in your own projects before the day.
Search all questions by round, difficulty and level, or save the ones you want to practise.
The problem: who used it and what it had to do, in one or two sentences.
The layers: frontend, API, database and hosting, each with the reason you picked it.
Proud part: one hard problem you solved and what you'd change now.
"The one I'd pick is a booking app for a small chain of fitness studios. Members book classes on the web, and staff manage schedules. I used React on the front, a Node API, and Postgres, because bookings, members and classes are tightly related and I needed transactions so two people couldn't take the last spot. It ran in containers on a single cloud provider with a managed database. The part I'm proudest of is the booking step. I first checked capacity in code and then inserted, and under load we got overbookings. I moved the check into one transaction with a row lock on the class, and the problem went away. If I built it again, I'd add proper monitoring from day one, because I found that bug from a customer email, not an alert."
Reciting a list of technologies with no reasons, or describing a team project without making clear which parts you built.
Why full stack: what you like about owning a feature from data to screen.
Honest strength: the side you're deeper on, with a concrete example.
The gap: how you're building up the weaker side.
"What I like most is owning a feature from the table to the button. When I understand both sides, I can make better trade-offs, like shaping an API around what the screen actually needs, or noticing that a slow page is really a missing index. Honestly, I'm stronger on the backend. I'm comfortable designing schemas, writing queries and debugging APIs under load. On the frontend I'm solid with components, state and forms, but I'm less experienced with fine-grained accessibility and animation work. I've been closing that gap by taking the UI tickets on purpose and asking our designer to review my screens. I think most full stack developers lean one way, and I'd rather tell you which way I lean than pretend I don't."
Claiming to be equally expert at everything, or suggesting you went full stack because you couldn't choose.
Start from the user: what the screen needs to show and do.
Agree the contract: the API request and response shape, written down early.
Build in slices: a thin working path through every layer, then fill it out.
"I start from what the user needs to see and do, usually by looking at the design and listing the data each screen needs. From that I sketch the API contract: the endpoints, the request and response shapes, and the errors. That contract is the thing I want agreed early, because it lets frontend and backend work move in parallel and it shows me what the data model needs. Then I build one thin slice through every layer, say creating one item from the form all the way to the table and back, before I add editing, validation and edge cases. Getting a skinny version working end to end early surfaces the surprises, like a field the design assumes but the data doesn't have."
Building the whole backend first in isolation and only discovering at the end that the screens need different data.
Situation: the unfamiliar area and why it fell to you.
How you learned: the docs, the existing code, and who you asked.
Result: what you shipped and how you made sure it was safe.
"In my first year, our only infrastructure person left and I had to change how our app was deployed so we could add a background worker. I'd never written a deployment config. I started by reading our existing setup line by line and writing down what each part did, then went through the official docs for just those pieces. I found someone in another team who knew the platform and asked for thirty minutes to review my plan, which saved me from a mistake with health checks. I tried everything on staging first, deployed during a quiet hour, and wrote a short guide so the next person wouldn't start from zero. It went out in about a week, and that guide is still what new people read."
Copying config from the internet into production without understanding it, or waiting weeks without asking anyone.
Browser: client-side validation, then a request with a body and the session cookie or token.
Network and edge: DNS and TLS, often reused, then a load balancer or proxy.
Server: routing, auth check, validation, business logic, a database write.
Back to the UI: status code and JSON, then update the screen or show the error.
"When they click Save, the click handler runs, checks the fields in the browser, and sends a POST with a JSON body. The browser attaches the session cookie, or the app adds a token header. If this isn't the first request, DNS is cached and the connection is already open, so it goes straight to the load balancer, which picks an app server. On the server, the router matches the path, middleware parses the body and checks who the user is and whether they're allowed to do this. Then I validate the input again, because the browser check can be skipped. The handler writes to the database, usually in a transaction, and returns a 201 with the saved record. Back in the browser, the code updates the local state so the list shows the new item, and shows a confirmation. If anything fails, the status code tells the UI which message to show."
Skipping the server-side auth and validation step, or not being able to say what the browser actually sends.
Why: the browser blocks cross-origin reads unless the server's response allows that origin; curl has no such rule.
Preflight: JSON bodies or auth headers trigger an OPTIONS request the server must answer.
Fix: allow the exact frontend origin, and with cookies allow credentials, never a wildcard.
"CORS is a rule the browser enforces, based on headers the server sends back. By default a page can't read a response from a different origin unless that response says it's allowed. Curl isn't a browser, so it just shows the response. For requests with a JSON content type or an Authorization header, the browser first sends an OPTIONS preflight asking which origins, methods and headers are allowed, and if the API doesn't answer that properly, the real request never goes out. The fix is on the API: return Access-Control-Allow-Origin with the frontend's exact origin, answer preflights with the allowed methods and headers, and if we use cookies, allow credentials too, which rules out using a star. Another clean option is serving the API under the same origin through a reverse proxy, so CORS doesn't apply at all. What I wouldn't do is allow every origin just to make the error go away."
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com'], // the exact frontend origin
credentials: true, // needed when cookies are sent
}));
Thinking CORS is a server-side security wall, or fixing it by allowing every origin with credentials.
GraphQL gives: one endpoint, clients ask for exactly the fields they need, a typed schema.
GraphQL costs: harder HTTP caching, N+1 queries in resolvers, limits on expensive queries.
Decision: depends on how varied the clients' data needs are and what the team knows.
"It depends on how different the clients' needs are. GraphQL shines when several clients need different shapes of the same data, like a mobile screen that wants three fields and a web dashboard that wants thirty. The client asks for exactly what it needs in one round trip, and the schema is a typed contract both sides can rely on. But it moves complexity to the server. Plain HTTP caching is harder because most queries go through one endpoint, often as POST. Naive resolvers cause N+1 database queries, so you need batching. And since clients can write any query, you need depth or cost limits. For a small team with a few well-understood screens, I'd usually start with REST, well-designed resources and good docs, and move to GraphQL if we keep building one-off endpoints just to reshape data."
Saying GraphQL is simply faster or newer and therefore better, without naming any cost.
Additive first: add new fields, never remove or rename ones clients depend on.
Version when breaking: a new version in the URL or a header, with the old one kept running.
Retire safely: measure who still calls the old version, warn, then remove.
"First I'd see if the change can be additive. Adding a new field or a new endpoint doesn't break old clients, as long as they ignore fields they don't know, so I'd rather return the new shape alongside the old one than change the old one. If it's truly breaking, like a field changing type, I'd release a new version, either as a new path like v2 or through a header, and keep v1 working, ideally with v1 as a thin adapter over the new code so I'm not maintaining two copies of the logic. Then I'd log which app versions call v1, add a minimum-version check in the app so we can prompt people to update, and only switch v1 off when its traffic is close to zero. With mobile you have to assume some users won't update for months."
Changing the response in place and relying on everyone updating the app on release day.
Data: a comments table with post, author, body, created time and a status for moderation.
API: list comments for a post with pagination, create a comment, delete your own.
UI states: loading, empty, error, posting, and a failed post the user can retry.
"I'd add a comments table with an id, the post id, the author id, the body, a created time and a status, so we can hide spam without deleting it. It gets an index on post id and created time, since we always load comments for one post in order. For the API, a GET that lists comments for a post, paginated with a cursor so new comments don't shift the pages, a POST that creates one for the logged-in user, and a DELETE that only works for the author or a moderator. The server checks the length and stores the text exactly as written, and the frontend renders it as plain text so it's escaped on output. In the UI, I'd show a skeleton while loading, a friendly empty state, and an error with a retry button. When someone posts, I can show the comment straight away as 'posting', then confirm it, or mark it failed with a retry, keeping their text so they never lose what they wrote."
Designing only the table and endpoints and ignoring what the user sees when the request fails.
The choice: what you picked and why it made sense then.
The cost: what went wrong or got harder over time.
Now: what you'd do instead and the lesson you carry.
"On one project, we split a fairly small app into five services early, because we expected to grow fast and it felt like the professional way to build. In practice, the same four developers owned all five. A simple feature like adding a field to the user profile meant changes and deploys in three services, and debugging meant chasing a request through several logs. Local development needed a pile of containers just to start. Growth came slower than we planned, so we paid the cost without getting the benefit. If I did it again, I'd build one well-organised app with clear modules and split out a service only when a part genuinely needed to scale or deploy on its own. The lesson I carry is to design for the team and traffic you have, while keeping boundaries clean enough to split later."
Saying you'd change nothing, or blaming the decision entirely on someone else.
localStorage: easy, but any script on the page can read it, so one XSS bug leaks the token.
HttpOnly cookie: scripts can't read it, but the browser sends it automatically, so you must defend against CSRF.
Defences: Secure and SameSite flags, CSRF tokens or origin checks, short lifetimes, and still fixing XSS.
"If I put the token in localStorage, any JavaScript on the page can read it, so a single XSS hole, even in a third-party script, lets an attacker steal it and use it from anywhere. An HttpOnly cookie can't be read by scripts, which is why I prefer it. The catch is that the browser sends cookies automatically, so another site could trigger a request on the user's behalf. That's CSRF. I handle that with SameSite set to Lax or Strict, and for state-changing requests a CSRF token or a check of the Origin header. Keeping a short-lived access token only in memory, with a refresh token in an HttpOnly cookie, is another common pattern. Either way, HttpOnly doesn't make XSS harmless. Injected script can still make requests as the user while the page is open, so preventing XSS is still the real fix."
Saying HttpOnly cookies make the app safe from XSS, or putting tokens in localStorage without mentioning the XSS risk at all.
Sessions: a random id in a cookie, the state lives in a server-side store, so logout and revoking are instant.
JWTs: signed claims the server checks without a lookup, but a token stays valid until it expires.
Common middle: short-lived access tokens plus a refresh token you can revoke, or sessions for web and tokens for mobile.
"With sessions, the browser holds a random id in an HttpOnly cookie and the real session lives in a store like Redis or the database. Every request does a lookup, but logging someone out or banning them takes effect at once, because I just delete the session. A JWT is signed data the server can verify without any lookup, which is handy across several services and for mobile apps that don't handle cookies as naturally. The catch is revocation. A stolen or outdated token stays valid until it expires, so I keep access tokens short-lived, a few minutes, and pair them with a refresh token that's stored server-side and can be revoked. For this product I'd likely use a cookie session for the web app and short access tokens with refresh for mobile, both backed by the same user and session records."
Saying JWTs are simply more secure or scalable than sessions, or not knowing that a JWT can't be cancelled before it expires without extra work.
Redirect: to the provider with client id, redirect URL, scopes, a random state and a PKCE challenge.
Callback: provider sends back a short-lived code; you check state matches.
Exchange: your server trades the code for tokens and verifies the ID token.
Your session: find or create the user by the provider's id, then issue your own session.
"When they click the button, my app redirects them to the provider's authorize page with my client id, the exact redirect URL, the scopes I need, a random state value, and a PKCE code challenge. The user signs in there and agrees. The provider redirects back to my callback with a short-lived code and the same state. I check the state matches what I stored, which stops someone forcing their login onto my user. Then my server, not the browser, sends the code, my client secret and the PKCE verifier to the provider's token endpoint. I get back an access token and, with OpenID Connect, an ID token, which I verify: signature, issuer, audience and expiry. I look up my user by the provider's stable user id, create one if needed, and then issue my own session cookie. From then on, my app runs on its own session, not the provider's token."
Doing the code exchange in the browser with the client secret exposed, or skipping the state check.
What it is: attacker-controlled text rendered as HTML or script in another user's page.
Main defence: escape on output for the right context; frameworks do it unless you bypass them.
Extra layers: sanitise any HTML you must allow, a Content Security Policy, HttpOnly cookies.
"Cross-site scripting is when text an attacker controls ends up running as script in another user's browser. The classic case is a comment containing a script tag that's saved and then shown to everyone who opens the page. That script runs with the victim's session, so it can read the page and make requests as them. The main defence is escaping output for the context it lands in. Modern frameworks like React escape text by default, so the risk comes back when someone uses innerHTML or its equivalents. If we really must show user HTML, like rich text, I run it through a well-maintained sanitiser with an allow-list. On the backend I validate input, set a Content Security Policy that blocks inline scripts, and keep session cookies HttpOnly, which limits the damage if something slips through."
Saying input validation alone stops XSS, or that a framework makes it impossible no matter what the code does.
How it happens: user text is glued into the SQL string, so it can change the query itself.
Fix: parameterised queries or an ORM, so values travel separately from the SQL.
Gaps: column names and sort order can't be parameters, so use an allow-list.
"If the search endpoint builds the query by gluing the search text into the SQL string, the text becomes part of the command. Someone can type a quote followed by their own SQL and turn a simple search into a query that dumps every user, or worse. The fix is parameterised queries. I write the SQL with placeholders and pass the values separately, so the database always treats them as data, never as SQL. ORMs do this for you, until someone drops into a raw query. One gap people miss is that you can't use a placeholder for a column name or a sort direction, so if the user picks the sort column, I check it against a fixed list of allowed columns. I'd also give the app's database user only the permissions it needs."
// Unsafe: the search text becomes part of the SQL itself
const unsafe = `SELECT id, name FROM products WHERE name = '${term}'`;
// Safe: a parameterised query, the value is sent separately
const { rows } = await pool.query(
'SELECT id, name FROM products WHERE name = $1',
[term]
);
Relying on escaping quotes yourself, or believing an ORM makes injection impossible even in raw queries.
The flaw: hiding a button is UX, not security; anyone can call the API directly.
The fix: check the role on the server for every protected endpoint, from a verified token or the database.
Delivery: explain it with a quick demo, add a test, and offer to pair.
"I'd point out that hiding the buttons is fine for the interface, but it isn't a security check. Anyone can open the browser tools or use curl and call the admin endpoints directly, so if the API doesn't check the role, a normal user can do admin things. The server has to verify the token's signature and check the role on every protected route, ideally in shared middleware so no endpoint forgets. I'd also mention that a role inside a JWT stays valid until the token expires, so if we remove someone's admin rights we either keep tokens short-lived or check the role in the database for sensitive actions. I'd keep the tone practical, maybe show the curl call hitting the endpoint as a regular user, and suggest a test that makes sure non-admins get a 403."
Agreeing the change is fine because regular users can't see the buttons.
Relational: related data, joins, constraints and multi-row transactions.
Document: self-contained records read and written as one piece, with a shape that varies.
Access patterns: how the app reads and writes decides it, plus what the team can run well.
"I start from the data and how it's used. If the app has lots of things that relate to each other, like users, orders, products and payments, and I need joins, foreign keys and transactions across several rows, I go relational. That covers most business apps, and Postgres also has a JSON column type for the parts that genuinely vary. A document database fits when each record is mostly self-contained and read as one unit, like a product catalogue where each item has different attributes, or event data where the shape changes often. The trap is picking a document store to skip schema design and then rebuilding joins in application code. So my default for a new app is relational, and I'd need a clear access pattern to move away from it."
Saying document databases are chosen because they are schema-less and faster, with no mention of how the data is queried.
Core tables: users, products, orders, and order items as the join table.
Snapshot: store the unit price on each order item at the time of purchase.
Integrity and speed: foreign keys, checks, and indexes on the columns you filter by.
"I'd have users, products and orders, and then an order items table in between, because one order has many products and one product appears in many orders. Each order belongs to a user through a foreign key and has a status and a created time. Each order item points at the order and the product and holds the quantity. The key point is that it also stores the unit price at the moment of purchase. If I only linked to the product and read the current price, then changing a price tomorrow would silently change every past order's total, which is wrong for receipts, refunds and accounting. I'd add a check that quantity is positive, and index orders by user id because 'my orders' is the most common read."
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(10, 2) NOT NULL, -- price at the time of purchase
PRIMARY KEY (order_id, product_id)
);
CREATE INDEX orders_user_id_idx ON orders (user_id);
Putting a comma-separated list of product ids in the orders table, or reading the live product price for old orders.
Storage: files go in object storage; the database only keeps the key and metadata.
Direct upload: the server issues a short-lived signed URL; large files upload in parts.
After upload: confirm, check type and size, scan, and process in the background.
"I wouldn't stream big files through my API servers, and I'd never put them in the database. The browser asks my API for permission to upload, the API checks the user and the declared size and type, and returns a short-lived signed URL for object storage. The browser uploads straight to storage, and for multi-gigabyte videos it uses multipart upload, so a dropped connection only retries one part. When it finishes, the browser tells my API, which records the storage key, the owner and the status in the database. Then a background job checks the real file type rather than trusting the extension, scans it, makes thumbnails or transcodes the video, and marks it ready. Files are served through a CDN, and private ones get signed download links that expire."
Saving uploads to the app server's local disk or into a database column, or trusting the file extension.
The problem: during a rolling deploy, old and new code run against the same database at once.
Expand: add the new column, write to both, backfill old rows in batches.
Contract: switch reads, stop writing the old column, drop it in a later release.
"A straight rename breaks things, because during a deploy the old code is still running and still asks for the old column name. So I'd do it in steps across several releases. First, add the new column as nullable, which is a quick change. Next, release code that writes to both columns but still reads the old one. Then backfill the existing rows in small batches, so I don't lock the table or flood the database. Once the data matches, release code that reads from the new column. After that's been stable, stop writing the old column, and in a final release drop it. It's more work than one migration, but every step can be rolled back on its own, and at no point is running code looking for a column that isn't there."
Running a single rename migration during the deploy and accepting errors while servers restart.
Browser and CDN: long cache lifetimes for fingerprinted assets, short ones for pages or API responses.
Application cache: an in-memory store in front of expensive queries, with a TTL and invalidation on write.
Staleness: decide how fresh each piece must be, and never share per-user data in a shared cache.
"I'd look at it layer by layer. Static files like scripts and images get fingerprinted names and long cache lifetimes, so the browser and CDN serve them without touching my servers. The product data itself changes rarely, so the CDN or a reverse proxy can cache the page or the API response for a short time, and even a minute takes most of the load off. Behind that, I'd put an in-memory cache like Redis in front of the expensive queries, reading from the cache first and falling back to the database. For staleness, I decide per field. The description can be a few minutes old, but price and stock can't, so I either delete the cache key when the product is updated, or fetch stock separately and uncached. And anything per user, like the cart, must be marked private so a shared cache never serves one user's data to another."
Adding a cache with no plan for invalidation, or caching logged-in responses in a shared CDN.
Measure in the browser: the network tab shows which requests are slow and whether they wait on each other.
Split server time: logs or tracing show time in the handler versus the database.
Common causes: request waterfalls, N+1 queries, missing indexes, oversized payloads, heavy rendering.
"First I'd reproduce it with the browser's network tab and performance panel open. That tells me straight away if the eight seconds is one slow request, a chain of requests waiting on each other, a huge download, or the page busy rendering after the data arrives. If it's an API call, I check the server timing. With tracing or even simple timing logs, I can see how much is in our code and how much is database. Often it's a few classic things. A list endpoint that runs one query per row, the N+1 problem. A query missing an index, which I'd check with the database's explain plan. Or the dashboard calling five endpoints one after another when they could run in parallel. I fix the biggest measured cost first, re-measure, and set an alert so we know if it creeps back."
Jumping straight to adding a cache or a bigger server without measuring where the time goes.
On the pull request: lint, type checks, unit tests, a frontend build, integration tests against a real database.
On merge: build one artifact, deploy to staging, run migrations and smoke tests.
To production: same artifact, gradual rollout, health checks, and a one-step rollback.
"On every pull request, the pipeline runs linting, type checks and unit tests for both frontend and backend, builds the frontend, and runs integration tests against a real database in a container, because mocks hide query bugs. A preview environment for UI changes is a nice extra, so design and product can click through before merging. On merge to main, it builds one artifact, usually a container image, tags it with the commit, and deploys it to staging, where migrations run and a few smoke tests hit the key pages and endpoints. Production gets that exact same image, not a rebuild. I prefer a gradual rollout with health checks, so a bad release only reaches some traffic, and I want rollback to be one command. Secrets live in the platform's secret store, never in the repo."
Deploying by hand from a laptop, or rebuilding separately for production so what you tested isn't what ships.
Stop the harm: roll back or switch off the feature flag, then confirm errors drop.
Communicate: tell the team and support what's happening and when you'll update.
Investigate after: logs, error tracking, and why only some users were hit.
"Checkout is money, so my first move is to stop the damage, not to debug live. If the change is behind a feature flag, I switch it off. If not, I roll back, and I check the error rate actually drops. I'd post in the team channel straight away saying checkout is failing, that I'm rolling back, and when I'll update, so support knows what to tell customers. Then I investigate. The 'some users' part is a big clue with a cross-stack release. Often it's users still running the old frontend from cache calling the new API, or the new frontend hitting an API server that hasn't updated yet. So I'd look at the errors grouped by client version and server instance. Afterwards, I'd make the API accept both old and new request shapes during deploys."
Trying to find and patch the bug live in production while checkout keeps failing.
What happened: the impact on users, briefly.
Your part: what you did during the incident and in the fix.
What changed: the process or safeguard added afterwards.
"At my last company, a release I'd worked on caused password reset emails to stop going out for about two hours. I'd changed how we loaded config, and in production a variable for the email service came through empty, so the send failed quietly and only logged a warning. Support noticed first. I was the one who traced it, and we fixed the config and re-sent the missed emails to everyone who had asked in that window. I wrote the review with no blame, and said plainly that my change caused it. We made three changes afterwards: the app now refuses to start if a required setting is missing, failed email sends raise an alert instead of a warning, and we added a check after deploy that sends a real test email. Nothing like it has happened since."
Blaming others, or a story where the only lesson is to be more careful next time.
Understand why: what decision that figure helps the user make.
Find options: show it on the detail page, precompute it, compute it lazily, or ship without it first.
Agree and record: pick with design and product, and note any follow-up work.
"I'd first talk to the designer and the product manager to understand what the number is for. Sometimes it only matters for a few rows, or it's fine if it's an hour old. Then I'd come with options and their costs. We could show it only on the detail page, where computing it for one item is cheap. We could precompute it in a background job and store it, which makes the list fast but the figure slightly stale. We could load it lazily after the list appears. Or we could ship the list on Friday without it and add it the following week properly. What I wouldn't do is quietly compute it per row inside the list request and let the page crawl. Once we pick, I'd write down the decision and any follow-up ticket so it doesn't get lost."
Either refusing the design outright or quietly shipping something that makes the whole list slow.
Ask: who can use it, which fields are needed, and how many rows there are.
Protect: role checks on the server, only the needed fields, and a log of who exported what.
Scale: stream it or build it in a background job for large exports.
"It sounds like a one-hour job, so I'd ask a few quick questions first. Who should be able to use it? Which fields do they actually need, because exporting phone numbers and addresses has privacy rules attached? And how many users are we talking about? Then I'd build it with the permission check on the server, not just a hidden button. I'd export only the agreed fields, and log who ran it and when. If it's a few thousand rows, I can stream the file straight from the query. If it's hundreds of thousands, I'd run it as a background job that emails a short-lived download link, so the request doesn't time out and the database isn't hammered. I'd also neutralise values that start with characters like an equals sign, so the file can't run formulas when someone opens it in a spreadsheet."
Building it as a plain button that anyone with the URL can hit, returning every column of the users table.
The ask: what was requested and why it mattered to them.
Your concern: the cost in their terms, like time, speed or risk.
Outcome: the alternative you offered, what was decided, and what you learned.
"At my last job, product wanted live search that updated on every keystroke across all customer records, including notes. Our database couldn't do that quickly, and doing it properly meant adding a search engine, which was weeks of work plus something new to run. Instead of just saying no, I showed a quick prototype on a copy of real data where each keystroke took about two seconds, which made the problem obvious. I offered two options: live search on names and emails only, which I could ship that sprint, or the full thing in a later quarter. The product manager chose the first one, and it covered most of what support used it for. Later we did add proper search when notes search became a real need. What I learned is that showing the cost beats describing it."
A story where you simply refused, or where you complained but built it anyway without raising the cost.
Ask early: the edge cases and states the design doesn't show yet.
Build what's stable: start on the parts unlikely to change.
Show often: short demos to catch misunderstandings before they're expensive.
"I like being involved early, because I can flag things the mockups usually don't show. What happens when the list is empty, when a name is very long, when the request fails, or when the user doesn't have permission? Those questions often shape the design and the API. While the fuzzy parts get settled, I start on what's unlikely to change, like the data model and the basic endpoints, and I keep my assumptions written in the ticket so anyone can correct them. Then I show work early, even rough, in a quick demo or a preview link. It's much cheaper to hear 'that's not what I meant' on day two than after the feature's done. And if something is technically hard, I say so before it's designed in, not after."
Refusing to start until everything is specified, or guessing and building without telling anyone your assumptions.
Notice it: signs that work waits on you.
Spread it: short docs, pairing, and code reviews that teach.
Hand it over: let others own areas you used to own, and back them up.
"I notice it when people wait for me to review things only I understand, or when my holiday makes a release risky. That's a team problem, even if it feels flattering. I write short docs for the parts that confuse people most, like how a request is authenticated end to end, or how to run the app locally with real-looking data. When someone asks me the same question twice, that's a sign it should be written down. I pair on tricky tickets instead of taking them myself, and in code reviews I explain the why, not just 'change this'. Most of all, I hand areas over on purpose. I'll let a frontend-focused teammate own an API change with me as the reviewer, so next time they don't need me at all."
Seeing being the only one who knows something as job security.
Symptom: what users saw and where it seemed to come from.
Narrowing down: the evidence at each layer that moved you closer.
Cause and prevention: the real root, the fix, and the guard you added.
"At my last company, users reported that dates in their order history were sometimes a day off. It looked like a frontend formatting bug, and the first fix attempt changed the date display, which didn't help. I followed one bad order through the stack. The API response already had the wrong date, so it wasn't the browser. The database had the right date, so it wasn't the data either. The problem was in between. The column stored a date without a time zone, the server converted it to midnight in the server's time zone, and then the frontend converted it again to the user's. For users far from the server, midnight crossed a day boundary. We changed the API to return plain calendar dates as date strings, not timestamps, and added a test with a user in a far-off time zone."
A story where the fix was guessed and patched in the layer where the symptom appeared, with no root cause found.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.