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';
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