The new graduate
- Page 6
- 21 min
- Part 3: Five candidates, one role
Page 6 of 9 · Backend Engineer for AI products interviewsThe new graduate
1. Read the job description
2. How the interviews run
3. Five candidates, one role
4. Prepare for your own interview
Every year, many new graduates apply for roles that ask for two or three years of experience. They do it because the posting does not say “senior”, because there are few junior roles, and because sometimes it works. Many of them studied in a second language, in a new country, and did one internship while they studied.
This kind of candidate has no production story to tell. They have not been on call. They have not seen a system fail at 2 a.m. with real customers. But they often know the fundamentals better than people with five years of experience, because they studied them a few months ago: indexes, transactions, isolation levels. And they learn fast.
Minh is this candidate. We watch how Rafael changes the depth of his questions for a new graduate, and how Minh reasons about the cache bug from first principles. We also watch Minh ask for a minute to think, and stay silent. That is not a weak moment. You will see why.
The candidate
Minh Nguyen
New graduate, backend, one internship
- BSc Computer Science, May 2026
- 8-month internship
- Go and Python
- Toronto
- BSc Computer Science, Lakeshore University, 2022 to 2026. Courses in databases, distributed systems and operating systems
- Software Engineering Intern, Brindle (payments start-up, 60 people), 8 months
- Designed and implemented a high-throughput webhook processing service in Go for card payment events
- Fixed a duplicate-notification bug with idempotent event handling and an outbox table
- Final-year project: a small search engine for course notes in Python, with Postgres full-text search and pgvector
- Go, Python, FastAPI, PostgreSQL, Redis, Docker, GitHub Actions
What Rafael sees before the call
Section titled “What Rafael sees before the call”Rafael reads the CV next to the job description. The posting asks for four or more years of experience. Minh has eight months. Rafael does not stop there. Nora asked him to talk with Minh because Minh did very well in the practical coding round: he used the AI assistant, read every change it made, and found one wrong test fixture himself.
Two lines get a mark in Rafael’s notes. “Designed” a “high-throughput” service is a big claim for an intern. And “fixed a duplicate-notification bug” is interesting, because Casewell has a duplicate problem too.
He wants to find out:
- What did Minh build himself, and what did his mentor decide?
- Does he understand why his fix works, at the level of the database?
- How does he reason about a problem he has never seen, like the cache bug?
- Where is the edge, and what does he do there?
Terms on this page
Section titled “Terms on this page”- Tenant: one customer organisation in a shared system. At Casewell, each law firm is a tenant.
- Webhook: a request that another company’s system sends to your system when something happens, for example “payment received”.
- Idempotent: an operation that has the same result if it runs once or many times.
- Isolation level: how much one database transaction can see of other transactions that run at the same time. Postgres uses “read committed” by default.
- Unique constraint: a database rule that two rows cannot have the same value in a column or a set of columns.
- Outbox table: a table where you write “send this message” in the same transaction as your data change. A separate worker reads it and sends the message.
- Cache key: the value that a cache uses to find a stored result. Two requests with the same key get the same result.
- Row-level security (RLS): a Postgres feature that adds a filter to every query on a table, for example “only rows of the current tenant”.
- Prompt injection: text inside the input that tries to give the model new instructions.
The interview
Section titled “The interview”Minutes 0 to 5: the opening
Rafael(interviewer):
Hi Minh. Can you hear me okay?
Minh(candidate):
Yes, I can hear you. Hi. Thank you for the time.
Rafael(interviewer):
Good. I am Rafael, staff engineer here at Casewell. Thirteen years ago I was a new graduate too, so relax a little. The plan: first you tell me about something you built, and I ask many questions. Then I give you one real problem we have. Then your questions.
Minh(candidate):
Okay. Can I use the AI assistant like in the coding round?
Rafael(interviewer):
Not in this one. We had a round with AI already. This one is just talking. Paper is fine, if you like to draw.
Minh(candidate):
Okay, good. I have paper.
Minutes 5 to 25: the internship, then deeper
Rafael(interviewer):
So. Tell me about the webhook service.
Minh(candidate):
Okay. So, Brindle gives small shops a card terminal. When a payment happens, the card processor sends us a webhook. Payment authorised, payment captured, refund, these things. My team needed a service to receive them, save them, and tell the shop. In Go, with Postgres.
What I did is the receiving part and the database part. My mentor did the first design, and I built most of it. And later I fixed the duplicate bug.
Rafael(interviewer):
Okay. Your CV says “high-throughput”. How many events, per second?
Minh(candidate):
Hmm. Around 30,000 a day, I think. A day is about 86,000 seconds. So… less than one per second. Average. The peak was maybe 20 per second, in the evening.
High-throughput is maybe too big a word. My mentor said to write it like this.
Rafael(interviewer):
It is fine. Tell me about the duplicate bug.
Minh(candidate):
Okay. Some shops got the email “payment received” two times. Sometimes three. The shop owners were confused, they thought the customer paid two times.
The reason was, the processor retries if we don’t answer in about ten seconds. Our handler saved the event, sent the email, and then sometimes it was slow, so the processor send the same event again.
Rafael(interviewer):
But you saved the event. Why did the second one not see it?
Minh(candidate):
Yes, that is the interesting part. The code was: check if the event ID exists, if not, insert and send email. But two deliveries can arrive at almost the same time. Both check, both see nothing, both insert.
Rafael(interviewer):
Inside a transaction?
Minh(candidate):
Yes, inside a transaction. But Postgres default is read committed. One transaction cannot see the other’s insert before commit. So both checks say “no row”.
Rafael(interviewer):
And with repeatable read? Does that fix it?
Minh(candidate):
No. Repeatable read gives you a snapshot, so you still don’t see the other insert. And they insert two different rows, so there is no conflict. Serializable would catch it, I think. One transaction fails and you retry it. But what we did is simpler: a unique constraint on the event ID. Then INSERT ... ON CONFLICT DO NOTHING, and only the one that really inserted sends the email.
Rafael(interviewer):
Good. And the email itself? You send it inside the transaction?
Minh(candidate):
First we did, yes. Then my mentor showed me the outbox. In the same transaction, we insert a row “send this email”. Another worker reads the table and sends. So if the transaction rolls back, no email.
Rafael(interviewer):
And if the worker sends the email, and then crashes before it marks the row as sent?
Minh(candidate):
Then… it sends again. Hmm.
I think you cannot make the email exactly once. It is at least once. Some email providers accept a key, I think, so they can ignore the second one. But I did not check if ours did.
Rafael(interviewer):
That is the right answer. Most people say “exactly once” here and they are wrong. How did you know the fix worked?
Minh(candidate):
I wrote a test that sends the same event twenty times at the same time, with goroutines. Before the fix, it sent four or five emails. After, one. And in production we counted emails per event for two weeks. It was always one.
Rafael(interviewer):
Okay. One more. Anything you broke yourself? In the internship.
Minh(candidate):
Yes. I added an index on the events table. In a migration. And the inserts stopped for maybe forty seconds, because creating an index locks the table for writes. The processor started to retry. It was not a big problem, but my mentor saw the alert.
Now I know CREATE INDEX CONCURRENTLY. I have learned it that day.
Here Rafael notices something that matters for the rest of the call. Minh says “I don’t know” without fear, and he separates what he did from what his mentor did. For a new graduate, that honesty is worth more than a bigger project. Rafael moves to the Casewell problem.
Minutes 25 to 40: the Casewell problem
Rafael(interviewer):
Now our real problem. At the start of a deal, a law firm uploads up to 2,000 contracts at once. Today we process them inside the upload request; it times out, people upload again, we get duplicates. Last month one big upload slowed down every other customer for hours. And — I will be honest with you — we had an incident where one customer saw text from another customer’s document in a chat answer. It was a cache bug, fixed now. How would you redesign document processing and the chat so these things cannot happen?
Minh(candidate):
Okay. That is a lot. I start with the upload. How long does one contract take to process? Roughly.
Rafael(interviewer):
Depends on the length. Say 20 seconds for a normal one, with the model calls.
Minh(candidate):
So 2,000 times 20 seconds is 40,000 seconds. That is… eleven hours, if one by one. It can never fit in a request. The request should only save the file and answer fast. Like “202, accepted”, with a job ID. Then workers process in the background, and the page asks for progress.
Rafael(interviewer):
Okay. And the duplicates?
Minh(candidate):
It is the same as my webhook bug, I think. Hash the file. SHA-256 of the bytes. Unique constraint on tenant ID and hash. If the user uploads again, the insert does nothing and we return the same document.
Rafael(interviewer):
Why tenant ID in the constraint? Same file is same file, no?
Minh(candidate):
No, because two firms can have the same contract. Like the two sides of one deal. If the hash is global, firm B uploads, and we say “already exists”, and maybe we connect it to firm A’s document. That is a leak. Each firm must have its own copy.
Rafael(interviewer):
Good. Now the big upload that slowed everybody. One firm, 1,800 files, one shared queue. What do you do?
Minh(candidate):
Maybe one queue for each tenant? Then the workers take from every queue in turn.
Rafael(interviewer):
We have about 400 tenants. 400 queues in Celery?
Minh(candidate):
Hmm. I don’t know how Celery behaves with 400 queues. I never used Celery, only a little in a course.
Wait. Maybe not queues. Maybe a limit. Each tenant can have only, I don’t know, ten jobs running at one time. The other jobs wait in a table in Postgres, not in the queue.
Rafael(interviewer):
Okay. And who moves them from the table to the queue?
Minh(candidate):
A scheduler. Every few seconds it looks… for each tenant, how many are running, and adds more if under the limit.
I am guessing now. I have not built this. Also, what happens if a worker dies and the “running” count is wrong, I don’t know yet.
Rafael(interviewer):
It is a good guess. Honestly, we are discussing almost the same design right now. The dead-worker problem is real. People use a lease: the job has an expiry time, and the worker renews it while it runs. If it stops renewing, the job goes back.
Minh(candidate):
Ah, okay. Like a lock with a timeout.
Rafael then asks about retries. Minh proposes retries with a growing wait time between them, and a maximum number. After that, the job goes to a “failed” list that a person can look at. Rafael gives him the name, a dead-letter queue, and Minh says that he knew the idea but not the name. Rafael also asks about the documents that contain “Ignore previous instructions”. Minh compares it with SQL injection. Then he stops himself: “But with SQL you can escape the input. With the model you cannot, I think.” So the model should not be able to do anything dangerous, even if it follows the text. Rafael writes “good instinct, no experience” in his notes.
Minutes 40 to 50: the cache bug
Rafael(interviewer):
Now the part I care about most. I led the fix of this incident, so I know it well. The chat had a cache: similar question, then the passages we found last time. The key was made from the question text only. A customer asked “what is the termination notice period?”, and got passages from another customer’s contract. For about 40 minutes.
Why did this happen, and how do you make sure it cannot happen again?
Minh(candidate):
Can I take one minute to think?
Rafael(interviewer):
Of course. Take your time.
Minh(candidate):
Minh is silent for about a minute. He writes on his paper.
Okay. Sorry. I want to start from what a cache is.
Rafael(interviewer):
Go on.
Minh(candidate):
A cache says: same input, same output, so I can skip the work. That is only true if the key has everything that changes the output. The retrieved passages depend on the question, but also on which documents the user can see. The key had the question only. So one input was missing from the key.
Rafael(interviewer):
Mm-hm.
Minh(candidate):
So the key needs the tenant ID. But I think not only the tenant. If a firm uploads a new contract, the old cached passages are wrong now. So also a version of the document set. And maybe the user, if two lawyers in the same firm can see different contracts. Can they?
Rafael(interviewer):
Yes, sometimes. Law firms have… how do you say… ethical walls. A lawyer on one matter must not see documents of another matter, inside the same firm.
Minh(candidate):
Then the key needs the permission scope too. Or the cache must be per user. That makes the cache less useful, but it is safer.
Rafael(interviewer):
Okay. And how would you test it? So it cannot come back.
Minh(candidate):
First a simple test. Two tenants, the same contract text but different names inside. Tenant A asks the question, so the cache is warm. Then tenant B asks the same question. The test checks that every passage B gets has B’s tenant ID.
Rafael(interviewer):
Why did nobody find this in testing before?
Minh(candidate):
Hmm. Maybe the test data has only one tenant? Then the bug cannot happen in the test. So the test fixtures should always have at least two tenants. Everywhere, by default.
Rafael(interviewer):
Laughs a little. Yes. That was exactly it. One tenant in all the fixtures.
Minh(candidate):
And one more idea. Maybe also check when you read. After the cache gives passages, check that each passage has the same tenant ID as the request. If not, drop it and send an alert. Then even if the key is wrong again, the user does not see it.
Rafael(interviewer):
That is two layers. Why both?
Minh(candidate):
Because the key is in one place, but somebody will add another cache next year. The check at the end also protects the next cache.
Rafael asks one more question here: “We have row-level security in Postgres. Is that not enough?” Minh says that he is not sure how RLS works with connection pools. He thinks that the tenant setting must be set for each transaction, not for the whole connection. Otherwise the next request on the same connection may get the wrong tenant. Rafael confirms this. Then Minh adds: “But the cache was in Redis, no? RLS cannot see Redis.” That was the whole incident in one sentence.
Minutes 50 to 60: Minh's questions, and the close
Minh(candidate):
I have two questions. First, the posting says four years. Why did you invite me?
Rafael(interviewer):
Honest question, honest answer. Your coding round was very good, and Nora wanted a second opinion. I cannot tell you the decision. But we also have a new-graduate program. I will mention it in my notes.
Minh(candidate):
Okay, thank you. Second question. If I join, who would I learn from? Like, does a new person get a mentor?
Rafael(interviewer):
Sorry, you froze a bit. Can you repeat?
Minh(candidate):
Sorry. If I join, does a new person get a mentor?
Rafael(interviewer):
Yes. In the program, one engineer is your mentor for the first six months, and you don’t go on call in that time. In the normal role, less structure.
Rafael(interviewer):
We are at time. Ben will contact you this week. Thank you, Minh. I enjoyed the cache part.
Minh(candidate):
Thank you. Me too. It was hard, but I learned something.
After the call: Rafael’s notes
Section titled “After the call: Rafael’s notes”Rafael writes his notes against the job description:
Python backend and APIs: some evidence. Mostly Go in the internship. Clear and correct about request and response design.
Postgres: strong evidence. Isolation levels, unique constraints, the index lock. Better than many senior candidates.
Queues, retries and background jobs: some evidence. Right instincts and an honest edge on per-tenant fairness. No experience with Celery or at scale.
Security and tenant isolation: strong evidence for his level. Reasoned about the cache key from first principles, proposed two layers and a test with two tenants. Saw that RLS does not cover Redis.
Production ownership: no evidence. Expected for a new graduate. He did not pretend.
LLM features: no evidence. Good instinct on prompt injection.
Communication: strong evidence. Asked for time, said “I am guessing” when he was. Corrected his own CV.
Recommendation: no for this role; strong yes for the new-graduate program. Nora, please look at the program. I would like to be his mentor, if possible.
What happened next
Section titled “What happened next”Nora read Rafael’s notes and the coding round report. The role needs someone who can own the upload pipeline soon, and Minh cannot do that yet. But the new-graduate program had places for January. Nora asked Ben to move Minh into it. Minh did not need to repeat any interview. He had one extra 30-minute call with Nora about the program.
Ben called Minh and explained it honestly. This is not the role that he applied for. It is a different level, with a lower salary band and a mentor. Minh asked for two days to think, and he asked one more question: “Will I work on the upload pipeline?” Nora said yes, with Rafael. Minh accepted the offer at the new-graduate level.
Applying for a role above your level is not a mistake. Sometimes the company has a better place for you, and the only way to find it is to have the conversation.
What to take from this
Section titled “What to take from this”- Fundamentals are evidence. Isolation levels and unique constraints carried a large part of Minh’s interview. Know them well enough to explain them with your own bug.
- Correct your own over-claims before the interviewer does. “High-throughput was maybe too big a word” made everything after it more believable.
- Asking for a minute is a strength. Say that you need it, then think. A minute of silence is normal in a good interview.
- First principles travel. “What does the output depend on?” works for any cache key, at any company. You do not need production experience to ask it.
- Say “I am guessing” when you are. Minh’s edge on per-tenant fairness did not hurt him, because he marked it clearly and named the weak point himself.
- Your English does not need to be perfect. Rafael judged the content. See our advice.
If your profile is like Minh’s
Section titled “If your profile is like Minh’s”You have less experience than the posting asks for. You can still be a serious candidate, if your few projects go deep. See No work experience in the role?.
- Take one bug from your internship or project and learn it completely. Why did it happen? What does the database do at each step? Which isolation level would change the result, and why? Practise the story out loud, in English, two or three times. Our lesson Explain decisions shows how.
- Fix your CV words. Replace every big adjective with a number that you can defend. See Portfolio anatomy.
- Build one small multi-tenant service. A FastAPI app with two tenants, Postgres with row-level security, and a Redis cache. Write a test that tries to read the other tenant’s data through every layer. This one project prepares you for most of the questions in this interview. Start with resource and endpoint design and automated endpoint tests.
- Learn one background-job tool by using it. Celery or a durable workflow tool, it does not matter which. Make a job fail on purpose, retry it, and send it to a dead-letter queue. Then you can talk about retries from experience, not from a blog post.
- Learn safe migrations. Index creation, adding a column, renaming. See migration safety.
- Ask about new-graduate programs. If a company has one, ask the recruiter early. It may be a better fit than the role in the posting.