Understanding Node.js architecture is crucial for building scalable applications. Let’s dive into the event loop, async operations, and how Node.js handles concurrency.

The Event Loop

Node.js uses a single-threaded event loop to handle asynchronous operations efficiently.

Event Loop Phases

// Simplified event loop phases
┌───────────────────────────┐
   ┌─────────────────────┐  
      Timers                // setTimeout, setInterval
   └─────────────────────┘  
   ┌─────────────────────┐  
      Pending Callbacks     // I/O callbacks
   └─────────────────────┘  
   ┌─────────────────────┐  
      Idle, Prepare         // Internal use
   └─────────────────────┘  
   ┌─────────────────────┐  
      Poll                  // Fetch new I/O events
   └─────────────────────┘  
   ┌─────────────────────┐  
      Check                 // setImmediate callbacks
   └─────────────────────┘  
   ┌─────────────────────┐  
      Close Callbacks       // socket.on('close')
   └─────────────────────┘  
└───────────────────────────┘

How It Works

// Example: Understanding execution order
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// Output: 1, 4, 3, 2
// Why: 
// - Synchronous code runs first (1, 4)
// - Microtasks (Promises) run before macrotasks (setTimeout)
// - Event loop processes callbacks

Asynchronous Operations

Callbacks

// Traditional callback
fs.readFile('file.txt', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data);
});

Promises

// Promise-based
fs.promises.readFile('file.txt')
  .then(data => console.log(data))
  .catch(err => console.error(err));

// Async/await
async function readFile() {
  try {
    const data = await fs.promises.readFile('file.txt');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

Event Emitters

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

myEmitter.on('event', (data) => {
  console.log('Event received:', data);
});

myEmitter.emit('event', 'Hello World');

Concurrency Model

Single-Threaded but Non-Blocking

// Node.js handles I/O asynchronously
const http = require('http');

// This doesn't block
http.get('http://example.com', (res) => {
  res.on('data', (chunk) => {
    console.log(chunk);
  });
});

// This continues immediately
console.log('Request sent, continuing...');

Worker Threads for CPU-Intensive Tasks

// For CPU-intensive operations
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.postMessage('Start calculation');
  worker.on('message', (result) => {
    console.log('Result:', result);
  });
} else {
  parentPort.on('message', (msg) => {
    // Heavy computation
    const result = performHeavyCalculation();
    parentPort.postMessage(result);
  });
}

Scaling Strategies

Cluster Module

const cluster = require('cluster');
const os = require('os');

if (cluster.isMaster) {
  const numWorkers = os.cpus().length;
  
  for (let i = 0; i < numWorkers; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.id} died`);
    cluster.fork(); // Restart worker
  });
} else {
  // Worker process
  require('./server.js');
}

Load Balancing

// PM2 for process management
// pm2 start app.js -i max

// Or use nginx/HAProxy for load balancing

Best Practices

1. Avoid Blocking the Event Loop

// Bad: Blocks event loop
function heavyComputation() {
  let result = 0;
  for (let i = 0; i < 10000000000; i++) {
    result += i;
  }
  return result;
}

// Good: Use worker threads or break into chunks
function asyncHeavyComputation() {
  return new Promise((resolve) => {
    setImmediate(() => {
      // Process in chunks
      resolve(computeChunk());
    });
  });
}

2. Use Streams for Large Data

// Bad: Loads entire file into memory
const data = fs.readFileSync('large-file.txt');

// Good: Stream processing
const stream = fs.createReadStream('large-file.txt');
stream.on('data', (chunk) => {
  processChunk(chunk);
});

3. Handle Errors Properly

// Always handle errors in async operations
async function fetchData() {
  try {
    const data = await fetch('http://api.example.com');
    return data;
  } catch (error) {
    // Log and handle error
    console.error('Fetch failed:', error);
    throw error;
  }
}

Performance Tips

1. Connection Pooling

// Database connection pooling
const pool = mysql.createPool({
  connectionLimit: 10,
  host: 'localhost',
  user: 'user',
  password: 'password',
  database: 'mydb'
});

2. Caching

// Use Redis for caching
const redis = require('redis');
const client = redis.createClient();

async function getCachedData(key) {
  const cached = await client.get(key);
  if (cached) return JSON.parse(cached);
  
  const data = await fetchData();
  await client.setex(key, 3600, JSON.stringify(data));
  return data;
}

3. Compression

// Enable gzip compression
const compression = require('compression');
app.use(compression());

Common Pitfalls

1. Callback Hell

// Bad: Nested callbacks
fs.readFile('file1.txt', (err, data1) => {
  fs.readFile('file2.txt', (err, data2) => {
    fs.readFile('file3.txt', (err, data3) => {
      // Deep nesting
    });
  });
});

// Good: Use async/await
async function readFiles() {
  const [data1, data2, data3] = await Promise.all([
    fs.promises.readFile('file1.txt'),
    fs.promises.readFile('file2.txt'),
    fs.promises.readFile('file3.txt')
  ]);
}

2. Unhandled Promise Rejections

// Always handle promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // Log and handle
});

Conclusion

Node.js architecture is powerful because:

  • Event-driven: Handles many connections efficiently
  • Non-blocking I/O: Doesn’t wait for operations
  • Single-threaded: Simpler concurrency model
  • Scalable: Can handle thousands of connections

Key takeaways:

  • Understand the event loop phases
  • Use async/await for cleaner code
  • Avoid blocking operations
  • Use worker threads for CPU-intensive tasks
  • Implement proper error handling
  • Scale with clustering or load balancing

Master these concepts to build high-performance Node.js applications! 🚀