Senior Fullstack Interview Playbook: Go Concurrency, System Design & STAR Behavioral Answers
Bài này có bản tiếng Việt. Đọc bản tiếng Việt →
Nguyen Hai Trinh • Senior Software Engineer @ SmileViet
Hi everyone. After a few rounds of interviews, from tech companies to fintech, I’ve noticed that what separates a Middle from a Senior Engineer isn’t how many technology keywords you know. It’s how you think about trade-offs, and whether you can trace a problem down to its root cause. Below are the questions where I got probed the hardest, along with the answers that worked for me.
1. Go Concurrency: understand it, don’t memorize it
Ask about Goroutines and Channels, and the follow-up is always the same: “How do you handle 100,000 tasks arriving at once without taking down the database or running out of memory?”
A good answer usually starts with the Worker Pool pattern: cap the number of goroutines running at the same time, and use a buffered channel as the job queue.
// Worker pool with a buffered channel to control the number of goroutines
func WorkerPool(jobs <-chan Job, results chan<- Result, workers int) {
var wg sync.WaitGroup
for w := 1; w <= workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- job.Execute()
}
}()
}
wg.Wait()
close(results)
}
The flow is simple: jobs go into one channel, N workers pull from it, and results come back on a second channel.
flowchart LR J["jobs (buffered channel)"] --> W1["worker 1"] J --> W2["worker 2"] J --> WN["worker N"] W1 --> R["results channel"] W2 --> R WN --> R
Things worth stressing in your answer:
- Backpressure: the buffered channel caps the number of waiting jobs, so memory can’t grow without bound. When the channel is full, the goroutine sending jobs blocks. That is a natural throttle, and you don’t need a separate rate limiter.
- Graceful shutdown: use
context.Contextto cancel the remaining jobs on timeout or when a signal (SIGTERM) arrives. A common mistake is forgetting to pass the context into the workers, so the process can’t stop cleanly during a deploy. - Observability: always expose metrics (queue depth, goroutine count, jobs in flight) through
pprofor Prometheus so you can debug production. If queue depth keeps climbing while throughput stays flat, the bottleneck is downstream (the DB, an external API), not the worker pool.
Channel vs Mutex: when to use which
This follow-up comes up all the time. My rule of thumb: use a Channel to pass ownership of data between goroutines, meaning whoever is “holding” the data at that moment. Use a Mutex to protect shared state that several goroutines read and write in place, like a counter or an in-memory cache.
// Use a Mutex when several goroutines read/write the same shared struct
type SafeCounter struct {
mu sync.Mutex
count map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.count[key]++
}
A trick that scores points: quote the classic Go line, “Do not communicate by sharing memory; instead, share memory by communicating”, then point out right away that in production, a simple counter with sync.Mutex or the atomic package is usually faster and easier to read than building a channel-based actor just to bump a number.
2. A 4-step framework for system design
When you answer in a structured way, the interviewer can follow your thinking without guessing what’s going on in your head:
- Clarify requirements — ask about traffic (average QPS, peak QPS), consistency requirements (strong or eventual), and the latency budget (what p99, in milliseconds) before you draw a single box. A lot of candidates lose points right here because they rush to draw an architecture before the problem is clear.
- Back-of-envelope estimation — estimate QPS, storage and bandwidth so you can pick the right kind of database or cache. For example: 10 million users, each creating 1 booking a day, each booking ~2KB → ~20GB/day, ~7TB/year. That number decides whether you need sharding from day one.
- High-level design — draw the main request flow (client → gateway → service → database/cache/queue), set clear service boundaries, and decide where to put the cache and queue to take load off downstream.
- Deep dive & trade-offs — pick the 1-2 hot spots the interviewer cares about most (for example idempotency on retries, or the sharding key for the booking table) and analyze the trade-offs concretely, with numbers, instead of just saying “scalable”.
Worked example: a high-volume ticketing system
A classic prompt: “Design a concert ticketing system where traffic jumps 50x in the 5 minutes after sales open.” Applying the framework:
- Step 1: ask whether temporary overselling is allowed and what level of consistency is required (never selling the same seat twice is a must).
- Step 2: estimate 100,000 tickets and 500,000 users waiting to buy at the same time. You need a queue at the entry layer so requests don’t flood straight into the DB.
- Step 3: put a queue in front (for example a Redis List or Kafka), with a worker pool that processes requests sequentially per seat, so two requests picking the same seat can’t race.
- Step 4: deep dive into a “seat lock” using Redis
SETNXwith a short TTL (say 5 minutes) to hold the seat while the user enters payment details. When the TTL runs out, the seat is released automatically if they didn’t pay in time.
Put the four steps together and you get this flow:
flowchart LR A["1. Clarify<br/>is overselling allowed?<br/>what level of consistency?"] --> B["2. Estimate<br/>100,000 tickets, 500,000 users at once"] B --> C["3. High-level<br/>queue in front, worker pool<br/>processes sequentially per seat"] C --> D["4. Deep dive<br/>Redis SETNX seat lock, 5-minute TTL"]
3. The behavioral round: STAR with numbers
For the behavioral round, use the STAR structure (Situation, Task, Action, Result) and always attach numbers. “I optimized the API” is forgotten the moment it’s said. “I used a Caffeine cache with a goroutine pool, latency went from 5s to 200ms, and database CPU dropped 40%” is something an interviewer remembers.
A complete STAR answer
Situation: At my previous company, the payment system started timing out in bulk during peak hours, and customers were reporting that they couldn’t pay.
Task: I was asked to investigate and fix it within 2 days, before a big promotion.
Action: Tracing the logs, I found the database connection pool was exhausted, because every request opened its own transaction with no timeout. I added context.WithTimeout to every query, reduced the connection pool size to something sensible, and added a cache for the queries that were read over and over (product info, exchange rates).
Result: p99 latency dropped from 5 seconds to 200ms, the timeout error rate fell from 12% to under 0.1%, and the system handled the promotion at 8 times normal traffic without adding servers.
Concrete numbers beat vague adjectives every time. Prepare 3-4 STAR stories on different themes, one about performance, one about an incident, one about leadership/mentoring, so you’re not caught off guard whichever way the interviewer goes.
Keep reading
Need a developer for a contract or freelance project?
I take on fullstack projects — Go backends, React/Next.js frontends, or both ends. Send a few lines about the scope and timeline and I'll reply within 48 hours.