Phỏng Vấn & Sự Nghiệp • 10 phút đọc • 02/10/2026

Bí kíp phỏng vấn Senior Fullstack: Go Concurrency, System Design & Behavior STAR

Bài này có bản tiếng Anh. Read in English →

Nguyen Hai Trinh

Nguyen Hai Trinh • Senior Software Engineer @ SmileViet

Buổi phỏng vấn trong văn phòng
Ảnh: Tennessee Titans · CC BY 4.0

Chào mọi người. Qua mấy vòng phỏng vấn ở công ty công nghệ rồi tới fintech, mình thấy chỗ tách một Middle với một Senior Engineer không nằm ở số keyword công nghệ bạn biết. Nằm ở tư duy đánh đổi, và ở khả năng truy vấn đề tới gốc rễ. Mấy chỗ dưới đây là những câu mình bị hỏi sâu nhất, kèm cách trả lời mình thấy ăn.

1. Go Concurrency: hiểu bản chất, đừng thuộc lòng

Hỏi về Goroutine và Channel, câu đào sâu luôn là: “Làm thế nào để kiểm soát 100,000 tasks gửi đến cùng lúc mà không làm sập Database hoặc OOM memory?”

Câu trả lời tốt thường bắt đầu từ Worker Pool Pattern: giới hạn số goroutine chạy đồng thời bằng một buffered channel đóng vai trò hàng đợi công việc:

// Worker Pool với Buffered Channel để kiểm soát số lượng 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)
}

Luồng nó chỉ có vậy: job vào một channel, N worker lấy ra, kết quả trả về channel thứ hai.

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

Mấy thứ nên nhấn khi trả lời:

  • Backpressure: buffered channel giới hạn số job đang chờ, tránh memory tăng vô hạn. Nếu channel đầy, goroutine gửi job sẽ block — đó chính là cơ chế điều tiết tốc độ tự nhiên, không cần thêm rate limiter riêng.
  • Graceful shutdown: dùng context.Context để huỷ job còn lại khi timeout hoặc nhận signal (SIGTERM). Một lỗi hay gặp là quên truyền context vào worker, khiến process không thể dừng sạch khi deploy.
  • Observability: luôn expose metrics (queue depth, goroutine count, số job đang xử lý) qua pprof hoặc Prometheus để debug production. Khi queue depth tăng liên tục mà throughput không đổi, đó là dấu hiệu downstream (DB, API ngoài) đang là bottleneck chứ không phải worker pool.

Channel vs Mutex: khi nào dùng cái nào

Câu follow-up này gặp hoài. Nguyên tắc mình hay dùng: Channel để truyền ownership của dữ liệu giữa các goroutine, tức ai đang “cầm” dữ liệu tại thời điểm đó. Mutex để bảo vệ state dùng chung mà nhiều goroutine cùng đọc ghi tại chỗ — một counter, hay một cache in-memory.

// Dùng Mutex khi nhiều goroutine cùng đọc/ghi 1 struct dùng chung
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]++
}

Mẹo ghi điểm: nhắc câu kinh điển của Go, “Do not communicate by sharing memory; instead, share memory by communicating”, rồi chỉ ra luôn là ngoài production, một counter đơn giản dùng sync.Mutex hoặc atomic package thường nhanh và dễ đọc hơn việc dựng hẳn một channel-based actor chỉ để tăng một con số.

2. Framework 4 bước giải System Design

Trả lời có cấu trúc thì interviewer theo kịp mạch tư duy của bạn, khỏi phải đoán bạn đang nghĩ gì:

  1. Clarify requirements — hỏi lại traffic (QPS trung bình, QPS đỉnh), consistency requirement (strong hay eventual), latency budget (p99 bao nhiêu mili giây) trước khi vẽ bất kỳ box nào. Nhiều bạn mất điểm ngay từ bước này vì vội vẽ kiến trúc mà chưa rõ đề bài.
  2. Back-of-envelope estimation — ước lượng QPS, storage, băng thông để chọn đúng loại database/cache. Ví dụ: 10 triệu user, mỗi ngày tạo 1 booking, mỗi booking ~2KB → ~20GB/ngày, ~7TB/năm — con số này quyết định có cần sharding ngay từ đầu hay chưa.
  3. High-level design — vẽ luồng request chính (client → gateway → service → database/cache/queue), xác định service boundary rõ ràng và nơi đặt cache/queue để giảm tải cho downstream.
  4. Deep dive & trade-offs — chọn 1-2 điểm nóng mà interviewer quan tâm nhất (ví dụ: idempotency khi retry, chọn sharding key cho bảng booking) và phân tích đánh đổi cụ thể, có số liệu đi kèm thay vì chỉ nói chung chung “scalable”.

Ví dụ áp dụng: thiết kế hệ thống đặt vé số lượng lớn

Một đề bài kinh điển: “Thiết kế hệ thống bán vé concert, traffic tăng 50 lần trong 5 phút mở bán.” Áp dụng framework trên:

  • Bước 1: hỏi rõ có cho phép overselling tạm thời không, consistency ở mức nào (không được bán trùng ghế là bắt buộc).
  • Bước 2: ước lượng 100,000 vé, 500,000 user chờ mua cùng lúc → cần queue ở tầng vào để tránh request tràn thẳng vào DB.
  • Bước 3: thiết kế queue (ví dụ Redis List hoặc Kafka) đứng trước, một worker pool xử lý tuần tự theo ghế, tránh race condition khi hai request cùng chọn một ghế.
  • Bước 4: deep dive vào cơ chế “seat lock” bằng Redis SETNX với TTL ngắn (ví dụ 5 phút) để giữ ghế tạm trong lúc user nhập thông tin thanh toán, hết TTL tự động nhả ghế nếu không thanh toán kịp.

Ghép bốn bước lại thành một mạch thì nó ra thế này:

flowchart LR
  A["1. Clarify<br/>có cho phép overselling không?<br/>consistency mức nào?"] --> B["2. Estimate<br/>100,000 vé, 500,000 user cùng lúc"]
  B --> C["3. High-level<br/>queue đứng trước, worker pool<br/>xử lý tuần tự theo ghế"]
  C --> D["4. Deep dive<br/>seat lock Redis SETNX, TTL 5 phút"]

3. Vòng behavior: STAR kèm số liệu

Vòng behavior thì dùng cấu trúc STAR, tức Situation, Task, Action, Result, và nhớ kèm số. “Em đã tối ưu API” nghe xong là quên. “Em dùng Caffeine cache với goroutine pool, latency từ 5s xuống 200ms, CPU database giảm 40%” thì interviewer nhớ.

Ví dụ câu trả lời STAR đầy đủ

Situation: Hệ thống thanh toán của công ty cũ bị timeout hàng loạt vào giờ cao điểm, khách hàng report không thanh toán được.

Task: Mình được giao điều tra và fix trong vòng 2 ngày trước đợt khuyến mãi lớn.

Action: Mình trace log thấy connection pool tới database bị exhausted do mỗi request mở một transaction riêng mà không timeout. Mình thêm context.WithTimeout cho mọi query, giảm connection pool size xuống mức hợp lý, và thêm cache cho các query đọc lặp lại nhiều (thông tin sản phẩm, tỷ giá).

Result: p99 latency giảm từ 5 giây xuống 200ms, tỷ lệ lỗi timeout giảm từ 12% xuống dưới 0.1%, hệ thống chịu được đợt khuyến mãi với traffic gấp 8 lần ngày thường mà không cần scale thêm server.

Số cụ thể luôn thắng tính từ chung chung. Nên chuẩn bị sẵn 3-4 câu chuyện STAR ở các chủ đề khác nhau — một về performance, một về incident, một về leadership/mentor — để không bị động dù interviewer hỏi hướng nào.

Cần người cho dự án contract hoặc freelance?

Mình nhận dự án fullstack — backend Go, frontend React/Next.js, hoặc cả hai đầu. Gửi vài dòng về phạm vi công việc và thời gian, mình phản hồi trong 48 giờ.