Backend Interview Questions - Medium
Medium-level backend interview questions covering microservices, advanced databases, message queues, and performance optimization.
Q1: Explain microservices architecture and its trade-offs.
Answer:
Monolith vs Microservices
Advantages
- Independent deployment: Deploy services separately
- Technology flexibility: Different tech per service
- Scalability: Scale services independently
- Fault isolation: One service failure doesn't crash all
- Team autonomy: Teams own services
Disadvantages
- Complexity: Distributed system challenges
- Data consistency: No ACID across services
- Network latency: Inter-service communication
- Testing: Integration testing harder
- Deployment: More moving parts
Communication Patterns
1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/http"
8)
9
10// Synchronous (REST)
11func CreateOrderSync(userID string, items []Item) (*Order, error) {
12 // Call user service
13 userResp, err := http.Get(fmt.Sprintf("http://user-service/users/%s", userID))
14 if err != nil {
15 return nil, err
16 }
17 defer userResp.Body.Close()
18
19 var user User
20 json.NewDecoder(userResp.Body).Decode(&user)
21
22 // Call inventory service
23 checkReq := CheckInventoryRequest{Items: items}
24 reqBody, _ := json.Marshal(checkReq)
25
26 invResp, err := http.Post(
27 "http://inventory-service/check",
28 "application/json",
29 bytes.NewBuffer(reqBody),
30 )
31 if err != nil {
32 return nil, err
33 }
34 defer invResp.Body.Close()
35
36 var available bool
37 json.NewDecoder(invResp.Body).Decode(&available)
38
39 if available {
40 // Create order
41 return createOrderInDB(userID, items)
42 }
43
44 return nil, fmt.Errorf("items not available")
45}
46
47// Asynchronous (Message Queue)
48func CreateOrderAsync(userID string, items []Item, mq MessageQueue) (*Order, error) {
49 // Create order with pending status
50 order, err := createOrderInDB(userID, items)
51 if err != nil {
52 return nil, err
53 }
54 order.Status = "pending"
55
56 // Publish event
57 event := OrderCreatedEvent{
58 OrderID: order.ID,
59 UserID: userID,
60 Items: items,
61 }
62
63 if err := mq.Publish("order.created", event); err != nil {
64 return nil, err
65 }
66
67 return order, nil
68}
Q2: What are database transactions and ACID properties?
Answer:
Transaction Example
1BEGIN TRANSACTION;
2
3-- Deduct from account A
4UPDATE accounts SET balance = balance - 100 WHERE id = 1;
5
6-- Add to account B
7UPDATE accounts SET balance = balance + 100 WHERE id = 2;
8
9-- If both succeed, commit
10COMMIT;
11
12-- If any fails, rollback
13ROLLBACK;
Isolation Levels
Concurrency Problems
1// Dirty Read
2// Transaction 1
3UPDATE accounts SET balance = 1000 WHERE id = 1;
4// Transaction 2 reads balance = 1000 (uncommitted)
5ROLLBACK; // Transaction 1 rolls back
6
7// Non-Repeatable Read
8// Transaction 1
9SELECT balance FROM accounts WHERE id = 1; // Returns 100
10// Transaction 2 updates balance to 200 and commits
11SELECT balance FROM accounts WHERE id = 1; // Returns 200 (different!)
12
13// Phantom Read
14// Transaction 1
15SELECT COUNT(*) FROM accounts WHERE balance > 100; // Returns 5
16// Transaction 2 inserts new account with balance 200 and commits
17SELECT COUNT(*) FROM accounts WHERE balance > 100; // Returns 6 (phantom!)
Implementation
1package main
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7
8 _ "github.com/lib/pq"
9)
10
11func TransferMoney(ctx context.Context, db *sql.DB, fromID, toID string, amount float64) error {
12 // Begin transaction
13 tx, err := db.BeginTx(ctx, nil)
14 if err != nil {
15 return err
16 }
17 defer tx.Rollback() // Rollback if not committed
18
19 // Deduct from sender
20 result, err := tx.ExecContext(ctx,
21 `UPDATE accounts
22 SET balance = balance - $1
23 WHERE id = $2 AND balance >= $1`,
24 amount, fromID,
25 )
26 if err != nil {
27 return err
28 }
29
30 rowsAffected, _ := result.RowsAffected()
31 if rowsAffected == 0 {
32 return fmt.Errorf("insufficient funds")
33 }
34
35 // Add to receiver
36 _, err = tx.ExecContext(ctx,
37 `UPDATE accounts
38 SET balance = balance + $1
39 WHERE id = $2`,
40 amount, toID,
41 )
42 if err != nil {
43 return err
44 }
45
46 // Commit transaction
47 if err := tx.Commit(); err != nil {
48 return err
49 }
50
51 return nil
52}
53
54// Usage
55func main() {
56 db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable")
57 if err != nil {
58 panic(err)
59 }
60 defer db.Close()
61
62 ctx := context.Background()
63 if err := TransferMoney(ctx, db, "account1", "account2", 100.0); err != nil {
64 fmt.Println("Transfer failed:", err)
65 } else {
66 fmt.Println("Transfer successful")
67 }
68}
Q3: Explain message queues and when to use them.
Answer:
Architecture
Use Cases
1. Background Jobs:
1package main
2
3import (
4 "context"
5 "encoding/json"
6 "log"
7 "net/http"
8
9 "github.com/hibiken/asynq"
10)
11
12const TypeEmailDelivery = "email:deliver"
13
14type EmailPayload struct {
15 To string `json:"to"`
16 Template string `json:"template"`
17}
18
19// Producer: API endpoint
20func CreateUserHandler(w http.ResponseWriter, r *http.Request, client *asynq.Client) {
21 var req CreateUserRequest
22 json.NewDecoder(r.Body).Decode(&req)
23
24 // Create user in database
25 user := createUserInDB(req)
26
27 // Queue email sending (don't wait)
28 payload, _ := json.Marshal(EmailPayload{
29 To: user.Email,
30 Template: "welcome",
31 })
32
33 task := asynq.NewTask(TypeEmailDelivery, payload)
34 client.Enqueue(task)
35
36 w.WriteHeader(http.StatusCreated)
37 json.NewEncoder(w).Encode(user)
38}
39
40// Consumer: Worker process
41func HandleEmailDelivery(ctx context.Context, t *asynq.Task) error {
42 var payload EmailPayload
43 if err := json.Unmarshal(t.Payload(), &payload); err != nil {
44 return err
45 }
46
47 log.Printf("Sending email to %s with template %s", payload.To, payload.Template)
48 return sendEmail(payload.To, payload.Template)
49}
50
51// Worker setup
52func StartWorker() {
53 srv := asynq.NewServer(
54 asynq.RedisClientOpt{Addr: "localhost:6379"},
55 asynq.Config{Concurrency: 10},
56 )
57
58 mux := asynq.NewServeMux()
59 mux.HandleFunc(TypeEmailDelivery, HandleEmailDelivery)
60
61 if err := srv.Run(mux); err != nil {
62 log.Fatal(err)
63 }
64}
2. Event-Driven Architecture:
1// Order service publishes event
2await eventBus.publish('order.created', {
3 orderId: order.id,
4 userId: user.id,
5 total: order.total
6});
7
8// Multiple services subscribe
9// Inventory service
10eventBus.subscribe('order.created', async (event) => {
11 await inventory.reserve(event.orderId);
12});
13
14// Notification service
15eventBus.subscribe('order.created', async (event) => {
16 await notifications.send(event.userId, 'Order confirmed');
17});
18
19// Analytics service
20eventBus.subscribe('order.created', async (event) => {
21 await analytics.track('order_created', event);
22});
RabbitMQ Example
1const amqp = require('amqplib');
2
3// Producer
4async function publishMessage(queue, message) {
5 const connection = await amqp.connect('amqp://localhost');
6 const channel = await connection.createChannel();
7
8 await channel.assertQueue(queue, { durable: true });
9
10 channel.sendToQueue(
11 queue,
12 Buffer.from(JSON.stringify(message)),
13 { persistent: true }
14 );
15
16 await channel.close();
17 await connection.close();
18}
19
20// Consumer
21async function consumeMessages(queue, handler) {
22 const connection = await amqp.connect('amqp://localhost');
23 const channel = await connection.createChannel();
24
25 await channel.assertQueue(queue, { durable: true });
26 channel.prefetch(1); // Process one at a time
27
28 channel.consume(queue, async (msg) => {
29 const content = JSON.parse(msg.content.toString());
30
31 try {
32 await handler(content);
33 channel.ack(msg); // Acknowledge success
34 } catch (error) {
35 channel.nack(msg, false, true); // Requeue on failure
36 }
37 });
38}
Q4: What is database replication and sharding?
Answer:
Replication
Types:
- Master-Slave: One primary, multiple replicas
- Master-Master: Multiple primaries (bidirectional)
- Synchronous: Wait for replica confirmation
- Asynchronous: Don't wait for replicas
Sharding
Sharding Strategies:
1. Range-Based:
1function getShard(userId) {
2 if (userId <= 1000) return 'shard1';
3 if (userId <= 2000) return 'shard2';
4 return 'shard3';
5}
2. Hash-Based:
1function getShard(userId) {
2 const hash = hashFunction(userId);
3 const shardIndex = hash % NUM_SHARDS;
4 return `shard${shardIndex}`;
5}
3. Geographic:
1function getShard(userId) {
2 const user = getUser(userId);
3 if (user.country === 'US') return 'shard-us';
4 if (user.country === 'EU') return 'shard-eu';
5 return 'shard-asia';
6}
Implementation
1class ShardedDatabase {
2 constructor(shards) {
3 this.shards = shards; // Array of database connections
4 }
5
6 getShard(key) {
7 const hash = this.hash(key);
8 const index = hash % this.shards.length;
9 return this.shards[index];
10 }
11
12 async insert(key, data) {
13 const shard = this.getShard(key);
14 return await shard.query('INSERT INTO users VALUES ($1, $2)', [key, data]);
15 }
16
17 async get(key) {
18 const shard = this.getShard(key);
19 return await shard.query('SELECT * FROM users WHERE id = $1', [key]);
20 }
21
22 hash(key) {
23 // Simple hash function
24 let hash = 0;
25 for (let i = 0; i < key.length; i++) {
26 hash = ((hash << 5) - hash) + key.charCodeAt(i);
27 hash = hash & hash;
28 }
29 return Math.abs(hash);
30 }
31}
Q5: Explain API rate limiting strategies.
Answer:
Algorithms
1. Fixed Window:
1package main
2
3import (
4 "fmt"
5 "sync"
6 "time"
7)
8
9type FixedWindowLimiter struct {
10 mu sync.Mutex
11 limits map[string]int
12 limit int
13 window time.Duration
14}
15
16func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowLimiter {
17 return &FixedWindowLimiter{
18 limits: make(map[string]int),
19 limit: limit,
20 window: window,
21 }
22}
23
24func (f *FixedWindowLimiter) Allow(userID string) bool {
25 f.mu.Lock()
26 defer f.mu.Unlock()
27
28 now := time.Now()
29 windowStart := now.Truncate(f.window)
30 key := fmt.Sprintf("%s:%d", userID, windowStart.Unix())
31
32 count := f.limits[key]
33 if count >= f.limit {
34 return false
35 }
36
37 f.limits[key] = count + 1
38 return true
39}
2. Sliding Window:
1package main
2
3import (
4 "sync"
5 "time"
6)
7
8type SlidingWindowLimiter struct {
9 mu sync.Mutex
10 requests map[string][]time.Time
11 limit int
12 window time.Duration
13}
14
15func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowLimiter {
16 return &SlidingWindowLimiter{
17 requests: make(map[string][]time.Time),
18 limit: limit,
19 window: window,
20 }
21}
22
23func (s *SlidingWindowLimiter) Allow(userID string) bool {
24 s.mu.Lock()
25 defer s.mu.Unlock()
26
27 now := time.Now()
28 windowStart := now.Add(-s.window)
29
30 // Get and filter old requests
31 requests := s.requests[userID]
32 filtered := make([]time.Time, 0)
33 for _, t := range requests {
34 if t.After(windowStart) {
35 filtered = append(filtered, t)
36 }
37 }
38
39 if len(filtered) >= s.limit {
40 s.requests[userID] = filtered
41 return false
42 }
43
44 filtered = append(filtered, now)
45 s.requests[userID] = filtered
46 return true
47}
3. Token Bucket:
1package main
2
3import (
4 "sync"
5 "time"
6)
7
8type TokenBucket struct {
9 mu sync.Mutex
10 capacity float64
11 tokens float64
12 refillRate float64 // tokens per second
13 lastRefill time.Time
14}
15
16func NewTokenBucket(capacity, refillRate float64) *TokenBucket {
17 return &TokenBucket{
18 capacity: capacity,
19 tokens: capacity,
20 refillRate: refillRate,
21 lastRefill: time.Now(),
22 }
23}
24
25func (tb *TokenBucket) refill() {
26 now := time.Now()
27 elapsed := now.Sub(tb.lastRefill).Seconds()
28 tokensToAdd := elapsed * tb.refillRate
29
30 tb.tokens = min(tb.capacity, tb.tokens+tokensToAdd)
31 tb.lastRefill = now
32}
33
34func (tb *TokenBucket) Allow(tokens float64) bool {
35 tb.mu.Lock()
36 defer tb.mu.Unlock()
37
38 tb.refill()
39
40 if tb.tokens >= tokens {
41 tb.tokens -= tokens
42 return true
43 }
44
45 return false
46}
47
48func min(a, b float64) float64 {
49 if a < b {
50 return a
51 }
52 return b
53}
54
55// Usage with multiple users
56type RateLimiter struct {
57 mu sync.Mutex
58 buckets map[string]*TokenBucket
59 capacity float64
60 refillRate float64
61}
62
63func NewRateLimiter(capacity, refillRate float64) *RateLimiter {
64 return &RateLimiter{
65 buckets: make(map[string]*TokenBucket),
66 capacity: capacity,
67 refillRate: refillRate,
68 }
69}
70
71func (rl *RateLimiter) Allow(userID string) bool {
72 rl.mu.Lock()
73 bucket, exists := rl.buckets[userID]
74 if !exists {
75 bucket = NewTokenBucket(rl.capacity, rl.refillRate)
76 rl.buckets[userID] = bucket
77 }
78 rl.mu.Unlock()
79
80 return bucket.Allow(1)
81}
Redis Implementation
1const redis = require('redis');
2const client = redis.createClient();
3
4async function rateLimitRedis(userId, limit, windowSeconds) {
5 const key = `ratelimit:${userId}`;
6
7 const current = await client.incr(key);
8
9 if (current === 1) {
10 await client.expire(key, windowSeconds);
11 }
12
13 if (current > limit) {
14 const ttl = await client.ttl(key);
15 return {
16 allowed: false,
17 remaining: 0,
18 resetIn: ttl
19 };
20 }
21
22 return {
23 allowed: true,
24 remaining: limit - current
25 };
26}
27
28// Middleware
29function rateLimitMiddleware(limit, window) {
30 return async (req, res, next) => {
31 const userId = req.user?.id || req.ip;
32
33 const result = await rateLimitRedis(userId, limit, window);
34
35 res.set({
36 'X-RateLimit-Limit': limit,
37 'X-RateLimit-Remaining': result.remaining,
38 'X-RateLimit-Reset': result.resetIn
39 });
40
41 if (!result.allowed) {
42 return res.status(429).json({
43 error: 'Too many requests',
44 retryAfter: result.resetIn
45 });
46 }
47
48 next();
49 };
50}
51
52// Usage
53app.use('/api/', rateLimitMiddleware(100, 60)); // 100 requests per minute
Q6: What is connection pooling and why is it important?
Answer:
Without Connection Pool
With Connection Pool
Implementation
1package main
2
3import (
4 "context"
5 "database/sql"
6 "log"
7 "os"
8 "os/signal"
9 "syscall"
10 "time"
11
12 _ "github.com/lib/pq"
13)
14
15// Create connection pool
16func NewDB() (*sql.DB, error) {
17 connStr := "postgres://user:password@localhost:5432/mydb?sslmode=disable"
18 db, err := sql.Open("postgres", connStr)
19 if err != nil {
20 return nil, err
21 }
22
23 // Configure pool
24 db.SetMaxOpenConns(20) // Maximum connections
25 db.SetMaxIdleConns(5) // Minimum idle connections
26 db.SetConnMaxLifetime(time.Hour) // Max connection lifetime
27 db.SetConnMaxIdleTime(30 * time.Second) // Max idle time
28
29 // Test connection
30 if err := db.Ping(); err != nil {
31 return nil, err
32 }
33
34 return db, nil
35}
36
37// Use pool with explicit connection
38func GetUser(ctx context.Context, db *sql.DB, userID string) (*User, error) {
39 // Get connection from pool (automatic)
40 var user User
41 err := db.QueryRowContext(ctx,
42 "SELECT id, name, email FROM users WHERE id = $1",
43 userID,
44 ).Scan(&user.ID, &user.Name, &user.Email)
45
46 if err != nil {
47 return nil, err
48 }
49
50 return &user, nil
51}
52
53// Direct pool usage
54func GetAllUsers(ctx context.Context, db *sql.DB) ([]User, error) {
55 rows, err := db.QueryContext(ctx, "SELECT id, name, email FROM users")
56 if err != nil {
57 return nil, err
58 }
59 defer rows.Close()
60
61 var users []User
62 for rows.Next() {
63 var user User
64 if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
65 return nil, err
66 }
67 users = append(users, user)
68 }
69
70 return users, rows.Err()
71}
72
73// Graceful shutdown
74func main() {
75 db, err := NewDB()
76 if err != nil {
77 log.Fatal(err)
78 }
79
80 // Setup graceful shutdown
81 sigChan := make(chan os.Signal, 1)
82 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
83
84 go func() {
85 <-sigChan
86 log.Println("Shutting down gracefully...")
87 db.Close()
88 os.Exit(0)
89 }()
90
91 // Your application logic here
92 ctx := context.Background()
93 user, err := GetUser(ctx, db, "123")
94 if err != nil {
95 log.Fatal(err)
96 }
97 log.Printf("User: %+v", user)
98}
Monitoring
1// Pool events
2pool.on('connect', (client) => {
3 console.log('New client connected');
4});
5
6pool.on('acquire', (client) => {
7 console.log('Client acquired from pool');
8});
9
10pool.on('remove', (client) => {
11 console.log('Client removed from pool');
12});
13
14pool.on('error', (err, client) => {
15 console.error('Pool error:', err);
16});
17
18// Pool stats
19function getPoolStats() {
20 return {
21 total: pool.totalCount,
22 idle: pool.idleCount,
23 waiting: pool.waitingCount
24 };
25}
Q7: Explain API versioning strategies.
Answer:
URL Path Versioning
1// Version 1
2app.get('/api/v1/users', (req, res) => {
3 res.json({
4 users: [
5 { id: 1, name: 'Alice' }
6 ]
7 });
8});
9
10// Version 2 (breaking change: added email)
11app.get('/api/v2/users', (req, res) => {
12 res.json({
13 users: [
14 { id: 1, name: 'Alice', email: 'alice@example.com' }
15 ]
16 });
17});
Header Versioning
1function versionMiddleware(req, res, next) {
2 const version = req.headers['api-version'] || '1';
3 req.apiVersion = version;
4 next();
5}
6
7app.use(versionMiddleware);
8
9app.get('/api/users', (req, res) => {
10 if (req.apiVersion === '1') {
11 return res.json({ users: getUsersV1() });
12 }
13
14 if (req.apiVersion === '2') {
15 return res.json({ users: getUsersV2() });
16 }
17
18 res.status(400).json({ error: 'Unsupported API version' });
19});
Content Negotiation
1app.get('/api/users', (req, res) => {
2 const accept = req.headers.accept;
3
4 if (accept.includes('application/vnd.api.v1+json')) {
5 return res.json({ users: getUsersV1() });
6 }
7
8 if (accept.includes('application/vnd.api.v2+json')) {
9 return res.json({ users: getUsersV2() });
10 }
11
12 res.status(406).json({ error: 'Not acceptable' });
13});
Deprecation Strategy
1// Deprecation warning
2app.get('/api/v1/users', (req, res) => {
3 res.set({
4 'X-API-Deprecated': 'true',
5 'X-API-Deprecation-Date': '2024-12-31',
6 'X-API-Sunset-Date': '2025-06-30',
7 'Link': '</api/v2/users>; rel="successor-version"'
8 });
9
10 res.json({ users: getUsersV1() });
11});
12
13// Version lifecycle
14const versions = {
15 v1: { status: 'deprecated', sunsetDate: '2025-06-30' },
16 v2: { status: 'current' },
17 v3: { status: 'beta' }
18};
Q8: What is database connection leak and how to prevent it?
Answer:
Connection Leak Example
1// ❌ Connection leak
2async function badQuery(userId) {
3 const client = await pool.connect();
4
5 const result = await client.query(
6 'SELECT * FROM users WHERE id = $1',
7 [userId]
8 );
9
10 // Forgot to release!
11 return result.rows[0];
12}
13
14// After many requests, pool is exhausted
Prevention Strategies
1. Always Use try-finally:
1// ✅ Proper connection handling
2async function goodQuery(userId) {
3 const client = await pool.connect();
4
5 try {
6 const result = await client.query(
7 'SELECT * FROM users WHERE id = $1',
8 [userId]
9 );
10 return result.rows[0];
11 } finally {
12 client.release(); // Always released
13 }
14}
2. Use Pool Directly:
1// ✅ Pool handles connection automatically
2async function betterQuery(userId) {
3 const result = await pool.query(
4 'SELECT * FROM users WHERE id = $1',
5 [userId]
6 );
7 return result.rows[0];
8}
3. Wrapper Function:
1// ✅ Wrapper ensures cleanup
2async function withConnection(callback) {
3 const client = await pool.connect();
4
5 try {
6 return await callback(client);
7 } finally {
8 client.release();
9 }
10}
11
12// Usage
13const user = await withConnection(async (client) => {
14 const result = await client.query(
15 'SELECT * FROM users WHERE id = $1',
16 [userId]
17 );
18 return result.rows[0];
19});
Monitoring and Detection
1// Monitor pool health
2setInterval(() => {
3 const stats = {
4 total: pool.totalCount,
5 idle: pool.idleCount,
6 waiting: pool.waitingCount
7 };
8
9 console.log('Pool stats:', stats);
10
11 // Alert if pool is exhausted
12 if (stats.idle === 0 && stats.waiting > 0) {
13 console.error('WARNING: Connection pool exhausted!');
14 }
15}, 10000);
16
17// Set connection timeout
18const pool = new Pool({
19 connectionTimeoutMillis: 5000, // Fail fast
20 idleTimeoutMillis: 30000 // Close idle connections
21});
Q9: Explain database indexing strategies for complex queries.
Answer:
Composite Indexes
1-- Single column index
2CREATE INDEX idx_users_email ON users(email);
3
4-- Composite index (order matters!)
5CREATE INDEX idx_users_name_email ON users(last_name, first_name);
6
7-- Efficient for:
8SELECT * FROM users WHERE last_name = 'Smith'; -- ✓ Uses index
9SELECT * FROM users WHERE last_name = 'Smith' AND first_name = 'John'; -- ✓ Uses index
10
11-- Not efficient for:
12SELECT * FROM users WHERE first_name = 'John'; -- ✗ Doesn't use index
Partial Indexes
1-- Index only active users
2CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';
3
4-- Smaller index, faster queries for active users
5SELECT * FROM users WHERE email = 'alice@example.com' AND status = 'active';
Expression Indexes
1-- Index on computed value
2CREATE INDEX idx_users_lower_email ON users(LOWER(email));
3
4-- Efficient for case-insensitive search
5SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
Covering Indexes
1-- Include additional columns
2CREATE INDEX idx_users_email_name ON users(email) INCLUDE (first_name, last_name);
3
4-- Query can be satisfied entirely from index (no table lookup)
5SELECT first_name, last_name FROM users WHERE email = 'alice@example.com';
Full-Text Search
1-- Create GIN index for full-text search
2CREATE INDEX idx_posts_content_fts ON posts USING GIN(to_tsvector('english', content));
3
4-- Full-text search
5SELECT * FROM posts
6WHERE to_tsvector('english', content) @@ to_tsquery('english', 'database & performance');
Query Optimization
1-- Analyze query plan
2EXPLAIN ANALYZE
3SELECT * FROM users
4WHERE email = 'alice@example.com'
5AND status = 'active';
6
7-- Output shows:
8-- - Index Scan vs Seq Scan
9-- - Estimated vs actual rows
10-- - Execution time
Q10: What is the N+1 query problem and how to solve it?
Answer:
Problem Example
1// ❌ N+1 Problem
2async function getPostsWithAuthors() {
3 // 1 query: Get all posts
4 const posts = await db.query('SELECT * FROM posts LIMIT 10');
5
6 // N queries: Get author for each post
7 for (const post of posts) {
8 post.author = await db.query(
9 'SELECT * FROM users WHERE id = $1',
10 [post.author_id]
11 );
12 }
13
14 return posts;
15}
16
17// Total: 1 + 10 = 11 queries!
Solution 1: JOIN
1// ✅ Single query with JOIN
2async function getPostsWithAuthors() {
3 const result = await db.query(`
4 SELECT
5 posts.*,
6 users.id as author_id,
7 users.name as author_name,
8 users.email as author_email
9 FROM posts
10 JOIN users ON posts.author_id = users.id
11 LIMIT 10
12 `);
13
14 return result.rows;
15}
16
17// Total: 1 query
Solution 2: Eager Loading
1// ✅ Load all authors at once
2async function getPostsWithAuthors() {
3 // 1 query: Get posts
4 const posts = await db.query('SELECT * FROM posts LIMIT 10');
5
6 // 1 query: Get all authors
7 const authorIds = posts.map(p => p.author_id);
8 const authors = await db.query(
9 'SELECT * FROM users WHERE id = ANY($1)',
10 [authorIds]
11 );
12
13 // Map authors to posts
14 const authorMap = new Map(authors.rows.map(a => [a.id, a]));
15 posts.forEach(post => {
16 post.author = authorMap.get(post.author_id);
17 });
18
19 return posts;
20}
21
22// Total: 2 queries (much better!)
Solution 3: DataLoader (Batching)
1const DataLoader = require('dataloader');
2
3// Create DataLoader
4const userLoader = new DataLoader(async (userIds) => {
5 const users = await db.query(
6 'SELECT * FROM users WHERE id = ANY($1)',
7 [userIds]
8 );
9
10 const userMap = new Map(users.rows.map(u => [u.id, u]));
11 return userIds.map(id => userMap.get(id));
12});
13
14// Usage
15async function getPostsWithAuthors() {
16 const posts = await db.query('SELECT * FROM posts LIMIT 10');
17
18 // DataLoader batches these into single query
19 await Promise.all(
20 posts.map(async (post) => {
21 post.author = await userLoader.load(post.author_id);
22 })
23 );
24
25 return posts;
26}
ORM Example (Sequelize)
1// ❌ N+1 with ORM
2const posts = await Post.findAll();
3for (const post of posts) {
4 post.author = await post.getAuthor(); // N queries
5}
6
7// ✅ Eager loading with ORM
8const posts = await Post.findAll({
9 include: [{ model: User, as: 'author' }]
10});
Summary
Medium backend topics:
- Microservices: Architecture, trade-offs, communication
- ACID: Transactions, isolation levels, concurrency
- Message Queues: Async processing, decoupling, reliability
- Replication & Sharding: Read/write scaling, strategies
- Rate Limiting: Algorithms, Redis implementation
- Connection Pooling: Performance, leak prevention
- API Versioning: Strategies, deprecation
- Connection Leaks: Detection, prevention
- Index Strategies: Composite, partial, covering
- N+1 Problem: Detection, solutions, DataLoader
These concepts enable building scalable, performant backend systems.
Related Snippets
- Backend Interview Questions - Easy
Easy-level backend interview questions covering HTTP, REST APIs, databases, and … - Backend Interview Questions - Hard
Hard-level backend interview questions covering distributed systems, advanced …