50+ Microservices Node.js Pytania Rekrutacyjne 2026: RabbitMQ, Kafka i Saga

50+ Microservices Node.js Pytania Rekrutacyjne 2026: RabbitMQ, Kafka i Saga

Sławomir Plamowski 17 min czytania
api-gateway architektura backend kafka microservices nodejs pytania-rekrutacyjne rabbitmq

Microservices w Node.js to temat, który pojawia się na niemal każdej rozmowie na stanowisko mid/senior backend. Rekruterzy nie pytają już "czy znasz microservices" - sprawdzają czy rozumiesz trade-offy, potrafisz zaprojektować komunikację między serwisami, i wiesz jak obsłużyć awarie w systemie rozproszonym.

Ten przewodnik to kompendium wiedzy o microservices w ekosystemie Node.js - od podstaw przez komunikację (RabbitMQ, Kafka), wzorce (Saga, Circuit Breaker), po monitoring i observability. Wszystko czego potrzebujesz przed rozmową rekrutacyjną.

Spis treści

  1. Podstawy Microservices Pytania
  2. Komunikacja między Serwisami Pytania
  3. Message Brokers RabbitMQ vs Kafka Pytania
  4. API Gateway Pytania
  5. Wzorce Microservices Pytania
  6. Event-Driven Architecture Pytania
  7. Database per Service Pytania
  8. Monitoring i Observability Pytania
  9. Powiązane Artykuły

Podstawy Microservices Pytania

Czym Są Microservices?

Microservices to styl architektoniczny, gdzie aplikacja składa się z małych, niezależnych serwisów:

  • Każdy serwis ma własną bazę danych (database per service)
  • Serwisy komunikują się przez API (REST, gRPC) lub eventy (message brokers)
  • Każdy serwis można deployować, skalować i rozwijać niezależnie
  • Serwisy są zorganizowane wokół capability biznesowych (bounded contexts)

Monolit vs Microservices

MONOLIT:
┌─────────────────────────────────────────────┐
│                  Aplikacja                   │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐        │
│  │  Users  │ │ Orders  │ │Products │        │
│  └─────────┘ └─────────┘ └─────────┘        │
│                    │                         │
│              ┌─────┴─────┐                   │
│              │  Database │                   │
│              └───────────┘                   │
└─────────────────────────────────────────────┘

MICROSERVICES:
┌──────────┐   ┌──────────┐   ┌──────────┐
│  Users   │   │  Orders  │   │ Products │
│ Service  │   │ Service  │   │ Service  │
└────┬─────┘   └────┬─────┘   └────┬─────┘
     │              │              │
┌────┴────┐   ┌────┴────┐   ┌────┴────┐
│Users DB │   │Orders DB│   │Prod. DB │
└─────────┘   └─────────┘   └─────────┘

Kiedy Microservices, Kiedy Monolit?

Zostań przy monolicie gdy:

  • Zespół < 10 osób
  • Nowy projekt (MVP, startup)
  • Brak doświadczenia z distributed systems
  • Brak dedykowanego DevOps/Platform team
  • Domena nie jest dobrze zrozumiana

Rozważ microservices gdy:

  • Zespół > 15-20 osób, trudno koordynować pracę
  • Różne części systemu wymagają różnego skalowania
  • Potrzebujesz niezależnych release cycles
  • Masz jasno zdefiniowane bounded contexts
  • Masz dojrzałe CI/CD i infrastructure

Zalety i Wady

Zalety Wady
Niezależne deployments Złożoność operacyjna
Skalowanie per serwis Distributed debugging
Polyglot (różne języki) Network latency
Izolacja awarii Data consistency
Łatwiejsze onboarding (mały serwis) Overhead komunikacji
Ownership per zespół Konieczność DevOps maturity

Komunikacja między Serwisami Pytania

Wybór metody komunikacji to jedna z najważniejszych decyzji w microservices.

Komunikacja Synchroniczna

REST API:

// orders-service - wywołuje users-service
const axios = require('axios');

async function getUser(userId) {
  try {
    const response = await axios.get(
      `http://users-service:3000/users/${userId}`,
      {
        timeout: 5000,
        headers: { 'X-Correlation-ID': correlationId }
      }
    );
    return response.data;
  } catch (error) {
    if (error.code === 'ECONNABORTED') {
      throw new Error('Users service timeout');
    }
    throw error;
  }
}

gRPC:

// Proto definition
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (stream User);
}

message GetUserRequest {
  string user_id = 1;
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

// Node.js client
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition);

const client = new userProto.UserService(
  'users-service:50051',
  grpc.credentials.createInsecure()
);

client.getUser({ user_id: '123' }, (err, user) => {
  if (err) throw err;
  console.log(user);
});

REST vs gRPC:

Aspekt REST gRPC
Format JSON (text) Protobuf (binary)
Wydajność Niższa Wyższa (2-10x)
Streaming Ograniczony Pełny (bidirectional)
Browser support Pełny Ograniczony (gRPC-web)
Debugging Łatwy (curl) Trudniejszy
Schema Opcjonalna (OpenAPI) Wymagana (proto)

Komunikacja Asynchroniczna

Serwisy komunikują się przez message broker bez czekania na odpowiedź:

┌──────────┐     ┌─────────────┐     ┌──────────┐
│  Orders  │────▶│   Message   │────▶│Inventory │
│ Service  │     │   Broker    │     │ Service  │
└──────────┘     └─────────────┘     └──────────┘
                       │
                       ▼
                 ┌──────────┐
                 │ Shipping │
                 │ Service  │
                 └──────────┘

Zalety asynchronicznej:

  • Loose coupling
  • Większa odporność na awarie
  • Natural load leveling
  • Łatwe dodawanie konsumentów

Wady:

  • Eventual consistency
  • Trudniejsze debugowanie
  • Złożoność obsługi błędów
  • Message ordering challenges

Message Brokers RabbitMQ vs Kafka Pytania

RabbitMQ

Tradycyjny message broker oparty na AMQP:

const amqp = require('amqplib');

// Publisher
async function publishOrder(order) {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const exchange = 'orders';
  const routingKey = 'order.created';

  await channel.assertExchange(exchange, 'topic', { durable: true });

  channel.publish(
    exchange,
    routingKey,
    Buffer.from(JSON.stringify(order)),
    {
      persistent: true,
      contentType: 'application/json',
      headers: { 'x-correlation-id': order.correlationId }
    }
  );

  await channel.close();
  await connection.close();
}

// Consumer
async function consumeOrders() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const exchange = 'orders';
  const queue = 'inventory-service.orders';
  const routingKey = 'order.*';

  await channel.assertExchange(exchange, 'topic', { durable: true });
  await channel.assertQueue(queue, { durable: true });
  await channel.bindQueue(queue, exchange, routingKey);

  // Prefetch - ile wiadomości na raz
  await channel.prefetch(10);

  channel.consume(queue, async (msg) => {
    const order = JSON.parse(msg.content.toString());

    try {
      await processOrder(order);
      channel.ack(msg);  // Potwierdź przetworzenie
    } catch (error) {
      // Nack - wróć do kolejki lub dead letter
      channel.nack(msg, false, false);
    }
  });
}

Exchange Types w RabbitMQ:

DIRECT:     routing_key musi dokładnie pasować
FANOUT:     broadcast do wszystkich bound queues
TOPIC:      pattern matching (order.*, order.#)
HEADERS:    routing po headers

Apache Kafka

Distributed log dla high-throughput streaming:

const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'orders-service',
  brokers: ['kafka1:9092', 'kafka2:9092']
});

// Producer
const producer = kafka.producer();

async function publishOrder(order) {
  await producer.connect();

  await producer.send({
    topic: 'orders',
    messages: [
      {
        key: order.userId,  // Partitioning key
        value: JSON.stringify(order),
        headers: {
          'correlation-id': order.correlationId
        }
      }
    ]
  });
}

// Consumer
const consumer = kafka.consumer({ groupId: 'inventory-service' });

async function consumeOrders() {
  await consumer.connect();
  await consumer.subscribe({ topic: 'orders', fromBeginning: false });

  await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
      const order = JSON.parse(message.value.toString());
      const correlationId = message.headers['correlation-id'].toString();

      console.log({
        partition,
        offset: message.offset,
        key: message.key.toString(),
        order
      });

      await processOrder(order);
    }
  });
}

RabbitMQ vs Kafka - Kiedy Który?

Aspekt RabbitMQ Kafka
Model Message broker Distributed log
Retencja Usuwa po konsumpcji Konfigurowana (dni/rozmiar)
Replay Nie Tak (offset reset)
Throughput ~50k msg/s ~1M msg/s
Latency Niższa Wyższa
Ordering Per queue Per partition
Use case Task queues, RPC Event streaming, analytics

Używaj RabbitMQ gdy:

  • Request-reply pattern
  • Task queues (workers)
  • Potrzebujesz routing (exchanges)
  • Niższy throughput, niższa latency

Używaj Kafka gdy:

  • Event sourcing
  • High-throughput streaming
  • Potrzebujesz replay events
  • Analytics, data pipelines
  • Multiple consumers tego samego streamu

API Gateway Pytania

API Gateway to single entry point dla wszystkich klientów:

                    ┌─────────────────┐
Mobile ────────────▶│                 │────▶ Users Service
                    │   API Gateway   │
Web ───────────────▶│                 │────▶ Orders Service
                    │  - Auth         │
Third Party ───────▶│  - Rate Limit   │────▶ Products Service
                    │  - Routing      │
                    │  - Caching      │────▶ Payments Service
                    └─────────────────┘

Express Gateway

// gateway/config.yml
http:
  port: 8080

apiEndpoints:
  users:
    host: '*'
    paths: ['/api/users', '/api/users/*']
  orders:
    host: '*'
    paths: ['/api/orders', '/api/orders/*']

serviceEndpoints:
  usersService:
    url: 'http://users-service:3000'
  ordersService:
    url: 'http://orders-service:3000'

policies:
  - jwt
  - rate-limit
  - proxy

pipelines:
  users:
    apiEndpoints:
      - users
    policies:
      - jwt:
          - action:
              secretOrPublicKey: ${JWT_SECRET}
      - rate-limit:
          - action:
              max: 100
              windowMs: 60000
      - proxy:
          - action:
              serviceEndpoint: usersService
              changeOrigin: true

BFF Pattern (Backend for Frontend)

Osobny backend dla każdego typu klienta:

┌────────┐     ┌─────────────┐
│ Mobile │────▶│ Mobile BFF  │───┐
└────────┘     └─────────────┘   │     ┌─────────────┐
                                 ├────▶│   Services  │
┌────────┐     ┌─────────────┐   │     └─────────────┘
│  Web   │────▶│   Web BFF   │───┤
└────────┘     └─────────────┘   │
                                 │
┌────────┐     ┌─────────────┐   │
│  IoT   │────▶│   IoT BFF   │───┘
└────────┘     └─────────────┘
// mobile-bff/routes/dashboard.js
// Agreguje dane z wielu serwisów, optymalizuje dla mobile

router.get('/dashboard', async (req, res) => {
  const userId = req.user.id;

  // Równoległe wywołania do serwisów
  const [user, orders, recommendations] = await Promise.all([
    usersService.getUser(userId),
    ordersService.getRecentOrders(userId, { limit: 3 }),  // Mniej dla mobile
    productsService.getRecommendations(userId, { limit: 5 })
  ]);

  // Agregacja i transformacja dla mobile
  res.json({
    user: {
      name: user.name,
      avatar: user.avatarUrl  // Tylko potrzebne pola
    },
    recentOrders: orders.map(o => ({
      id: o.id,
      total: o.total,
      status: o.status
      // Bez szczegółów produktów - oszczędność bandwidth
    })),
    recommendations: recommendations.map(p => ({
      id: p.id,
      name: p.name,
      price: p.price,
      thumbnail: p.thumbnailUrl  // Mniejsze obrazki dla mobile
    }))
  });
});

Wzorce Microservices Pytania

Circuit Breaker

Zapobiega kaskadowym awariom:

const CircuitBreaker = require('opossum');

// Funkcja wywołująca zewnętrzny serwis
async function callUsersService(userId) {
  const response = await axios.get(`http://users-service:3000/users/${userId}`);
  return response.data;
}

// Circuit breaker wrapper
const breaker = new CircuitBreaker(callUsersService, {
  timeout: 3000,           // Timeout dla wywołania
  errorThresholdPercentage: 50,  // % błędów do otwarcia
  resetTimeout: 30000,     // Czas do half-open
  volumeThreshold: 10      // Min wywołań przed oceną
});

// Event handlers
breaker.on('open', () => console.log('Circuit OPEN - rejecting calls'));
breaker.on('halfOpen', () => console.log('Circuit HALF-OPEN - testing'));
breaker.on('close', () => console.log('Circuit CLOSED - normal operation'));

// Fallback gdy circuit open
breaker.fallback(() => ({
  id: 'unknown',
  name: 'Guest User',
  cached: true
}));

// Użycie
async function getUser(userId) {
  try {
    return await breaker.fire(userId);
  } catch (error) {
    console.error('Circuit breaker error:', error.message);
    throw error;
  }
}

Stany Circuit Breaker:

      ┌─────────────────────────────────────────────┐
      │                                             │
      ▼           błędy < threshold                 │
   CLOSED ─────────────────────────────────────────▶│
      │                                             │
      │ błędy > threshold                           │
      ▼                                             │
    OPEN ──────────────────────────────────────────▶│
      │                     ▲                       │
      │ resetTimeout        │ błąd                  │
      ▼                     │                       │
  HALF-OPEN ────────────────┘                       │
      │                                             │
      │ sukces                                      │
      └─────────────────────────────────────────────┘

Saga Pattern

Zarządzanie transakcjami rozproszonymi:

// Orchestration Saga - centralny orchestrator

class OrderSaga {
  constructor(orderService, inventoryService, paymentService, shippingService) {
    this.orderService = orderService;
    this.inventoryService = inventoryService;
    this.paymentService = paymentService;
    this.shippingService = shippingService;
  }

  async execute(orderData) {
    const saga = {
      orderId: null,
      inventoryReserved: false,
      paymentProcessed: false,
      shippingScheduled: false
    };

    try {
      // Step 1: Create order
      const order = await this.orderService.create(orderData);
      saga.orderId = order.id;

      // Step 2: Reserve inventory
      await this.inventoryService.reserve(order.items);
      saga.inventoryReserved = true;

      // Step 3: Process payment
      await this.paymentService.charge(order.userId, order.total);
      saga.paymentProcessed = true;

      // Step 4: Schedule shipping
      await this.shippingService.schedule(order);
      saga.shippingScheduled = true;

      // Step 5: Confirm order
      await this.orderService.confirm(saga.orderId);

      return { success: true, orderId: saga.orderId };

    } catch (error) {
      // Compensating transactions (rollback)
      await this.compensate(saga, error);
      throw error;
    }
  }

  async compensate(saga, error) {
    console.log('Saga compensation started:', error.message);

    // Reverse order matters!
    if (saga.shippingScheduled) {
      await this.shippingService.cancel(saga.orderId);
    }

    if (saga.paymentProcessed) {
      await this.paymentService.refund(saga.orderId);
    }

    if (saga.inventoryReserved) {
      await this.inventoryService.release(saga.orderId);
    }

    if (saga.orderId) {
      await this.orderService.cancel(saga.orderId);
    }
  }
}

Choreography vs Orchestration:

CHOREOGRAPHY (event-driven):
Order ──▶ OrderCreated ──▶ Inventory ──▶ InventoryReserved ──▶ Payment ──▶ ...

Zalety: Loose coupling, brak SPOF
Wady: Trudne śledzenie, złożona logika kompensacji

ORCHESTRATION (centralny kontroler):
┌──────────────┐
│ Orchestrator │
└──────┬───────┘
       │
  ┌────┼────┬────────┐
  ▼    ▼    ▼        ▼
Order Inv Payment Shipping

Zalety: Łatwe śledzenie, jasna logika
Wady: Single point of failure, tight coupling do orchestratora

Retry z Exponential Backoff

async function withRetry(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 30000,
    factor = 2
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (attempt === maxRetries) {
        break;
      }

      // Sprawdź czy błąd jest "retryable"
      if (!isRetryable(error)) {
        throw error;
      }

      // Exponential backoff z jitter
      const delay = Math.min(
        baseDelay * Math.pow(factor, attempt) + Math.random() * 1000,
        maxDelay
      );

      console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
      await sleep(delay);
    }
  }

  throw lastError;
}

function isRetryable(error) {
  // Retry tylko dla transient errors
  const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'];
  const retryableStatuses = [408, 429, 500, 502, 503, 504];

  return retryableCodes.includes(error.code) ||
         retryableStatuses.includes(error.response?.status);
}

// Użycie
const result = await withRetry(
  () => axios.get('http://users-service/users/123'),
  { maxRetries: 3, baseDelay: 1000 }
);

Bulkhead Pattern

Izolacja zasobów - awaria jednego nie wpływa na inne:

const Bottleneck = require('bottleneck');

// Osobne limiter dla każdego serwisu
const usersLimiter = new Bottleneck({
  maxConcurrent: 10,      // Max równoległych wywołań
  minTime: 100,           // Min czas między wywołaniami
  reservoir: 100,         // Pula requestów
  reservoirRefreshAmount: 100,
  reservoirRefreshInterval: 60 * 1000  // Odświeżaj co minutę
});

const ordersLimiter = new Bottleneck({
  maxConcurrent: 20,
  minTime: 50
});

// Użycie
async function getUser(userId) {
  return usersLimiter.schedule(() =>
    axios.get(`http://users-service/users/${userId}`)
  );
}

async function getOrders(userId) {
  return ordersLimiter.schedule(() =>
    axios.get(`http://orders-service/users/${userId}/orders`)
  );
}

// Awaria users-service nie wpływa na orders-service
// Każdy ma własny pool zasobów

Event-Driven Architecture Pytania

Event Sourcing

Stan aplikacji jako sekwencja eventów:

// Event store
class EventStore {
  constructor(db) {
    this.db = db;
  }

  async append(streamId, events, expectedVersion) {
    const session = await this.db.startSession();

    try {
      await session.withTransaction(async () => {
        // Optimistic concurrency check
        const stream = await this.db.collection('streams').findOne(
          { _id: streamId },
          { session }
        );

        const currentVersion = stream?.version || 0;
        if (currentVersion !== expectedVersion) {
          throw new Error('Concurrency conflict');
        }

        // Append events
        const eventsToInsert = events.map((event, i) => ({
          streamId,
          version: currentVersion + i + 1,
          type: event.type,
          data: event.data,
          metadata: event.metadata,
          timestamp: new Date()
        }));

        await this.db.collection('events').insertMany(eventsToInsert, { session });

        // Update stream version
        await this.db.collection('streams').updateOne(
          { _id: streamId },
          {
            $set: { version: currentVersion + events.length },
            $setOnInsert: { createdAt: new Date() }
          },
          { upsert: true, session }
        );
      });
    } finally {
      await session.endSession();
    }
  }

  async getEvents(streamId, fromVersion = 0) {
    return this.db.collection('events')
      .find({ streamId, version: { $gt: fromVersion } })
      .sort({ version: 1 })
      .toArray();
  }
}

// Aggregate
class Order {
  constructor() {
    this.id = null;
    this.status = null;
    this.items = [];
    this.total = 0;
    this.version = 0;
  }

  // Rebuild state from events
  static fromEvents(events) {
    const order = new Order();
    for (const event of events) {
      order.apply(event);
    }
    return order;
  }

  apply(event) {
    switch (event.type) {
      case 'OrderCreated':
        this.id = event.data.orderId;
        this.status = 'created';
        this.items = event.data.items;
        break;

      case 'OrderConfirmed':
        this.status = 'confirmed';
        this.total = event.data.total;
        break;

      case 'OrderShipped':
        this.status = 'shipped';
        break;

      case 'OrderCancelled':
        this.status = 'cancelled';
        break;
    }
    this.version = event.version;
  }
}

CQRS (Command Query Responsibility Segregation)

Oddzielne modele do zapisu i odczytu:

// Command side - zapis przez event sourcing
class OrderCommandHandler {
  constructor(eventStore) {
    this.eventStore = eventStore;
  }

  async createOrder(command) {
    const orderId = generateId();

    const events = [
      {
        type: 'OrderCreated',
        data: {
          orderId,
          userId: command.userId,
          items: command.items
        },
        metadata: { correlationId: command.correlationId }
      }
    ];

    await this.eventStore.append(`order-${orderId}`, events, 0);
    return orderId;
  }

  async confirmOrder(command) {
    const events = await this.eventStore.getEvents(`order-${command.orderId}`);
    const order = Order.fromEvents(events);

    if (order.status !== 'created') {
      throw new Error('Order cannot be confirmed');
    }

    const newEvents = [
      {
        type: 'OrderConfirmed',
        data: { orderId: command.orderId, total: calculateTotal(order.items) }
      }
    ];

    await this.eventStore.append(`order-${command.orderId}`, newEvents, order.version);
  }
}

// Query side - zoptymalizowane read models
class OrderReadModel {
  constructor(db) {
    this.db = db;
  }

  // Projection - buduje read model z eventów
  async handleEvent(event) {
    switch (event.type) {
      case 'OrderCreated':
        await this.db.collection('orders_view').insertOne({
          _id: event.data.orderId,
          userId: event.data.userId,
          status: 'created',
          itemCount: event.data.items.length,
          createdAt: event.timestamp
        });
        break;

      case 'OrderConfirmed':
        await this.db.collection('orders_view').updateOne(
          { _id: event.data.orderId },
          { $set: { status: 'confirmed', total: event.data.total } }
        );
        break;
    }
  }

  // Zoptymalizowane query
  async getUserOrders(userId, { limit = 10, offset = 0 }) {
    return this.db.collection('orders_view')
      .find({ userId })
      .sort({ createdAt: -1 })
      .skip(offset)
      .limit(limit)
      .toArray();
  }
}

Database per Service Pytania

Outbox Pattern

Zapewnia atomowość zapisu do DB i publikacji eventu:

// Zamiast:
// 1. Save to DB
// 2. Publish event (może się nie udać!)

// Używamy:
// 1. Save to DB + save to outbox (jedna transakcja)
// 2. Osobny proces czyta outbox i publikuje

async function createOrder(orderData) {
  const session = await mongoose.startSession();

  try {
    await session.withTransaction(async () => {
      // 1. Zapisz order
      const order = await Order.create([orderData], { session });

      // 2. Zapisz event do outbox (ta sama transakcja!)
      await Outbox.create([{
        aggregateType: 'Order',
        aggregateId: order[0]._id,
        eventType: 'OrderCreated',
        payload: {
          orderId: order[0]._id,
          userId: orderData.userId,
          items: orderData.items
        },
        createdAt: new Date()
      }], { session });
    });
  } finally {
    await session.endSession();
  }
}

// Outbox processor (osobny proces/worker)
async function processOutbox() {
  while (true) {
    const events = await Outbox.find({ processedAt: null })
      .sort({ createdAt: 1 })
      .limit(100);

    for (const event of events) {
      try {
        await publishToMessageBroker(event);
        await Outbox.updateOne(
          { _id: event._id },
          { $set: { processedAt: new Date() } }
        );
      } catch (error) {
        console.error('Failed to publish event:', error);
        // Retry przy następnym cyklu
      }
    }

    await sleep(1000);
  }
}

Synchronizacja Danych

// Event consumer - aktualizuje lokalne read models
class ProductsEventConsumer {
  constructor(db, messageBroker) {
    this.db = db;
    this.messageBroker = messageBroker;
  }

  async start() {
    await this.messageBroker.subscribe('products.*', async (event) => {
      // Idempotency check
      const processed = await this.db.collection('processed_events')
        .findOne({ eventId: event.id });

      if (processed) {
        console.log('Event already processed:', event.id);
        return;
      }

      // Process event
      await this.handleEvent(event);

      // Mark as processed
      await this.db.collection('processed_events')
        .insertOne({ eventId: event.id, processedAt: new Date() });
    });
  }

  async handleEvent(event) {
    switch (event.type) {
      case 'ProductCreated':
        await this.db.collection('products_cache').insertOne({
          _id: event.data.productId,
          name: event.data.name,
          price: event.data.price,
          lastUpdated: event.timestamp
        });
        break;

      case 'ProductUpdated':
        await this.db.collection('products_cache').updateOne(
          { _id: event.data.productId },
          {
            $set: {
              name: event.data.name,
              price: event.data.price,
              lastUpdated: event.timestamp
            }
          }
        );
        break;

      case 'ProductDeleted':
        await this.db.collection('products_cache')
          .deleteOne({ _id: event.data.productId });
        break;
    }
  }
}

Monitoring i Observability Pytania

Distributed Tracing z OpenTelemetry

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');

// Setup
const sdk = new NodeSDK({
  serviceName: 'orders-service',
  traceExporter: new JaegerExporter({
    endpoint: 'http://jaeger:14268/api/traces'
  }),
  instrumentations: [getNodeAutoInstrumentations()]
});

sdk.start();

// Manual spans
const { trace } = require('@opentelemetry/api');

async function processOrder(order) {
  const tracer = trace.getTracer('orders-service');

  return tracer.startActiveSpan('processOrder', async (span) => {
    try {
      span.setAttribute('order.id', order.id);
      span.setAttribute('order.user_id', order.userId);

      // Child span dla inventory check
      await tracer.startActiveSpan('checkInventory', async (childSpan) => {
        await inventoryService.check(order.items);
        childSpan.end();
      });

      // Child span dla payment
      await tracer.startActiveSpan('processPayment', async (childSpan) => {
        await paymentService.charge(order);
        childSpan.end();
      });

      span.setStatus({ code: SpanStatusCode.OK });
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

Correlation ID

const { v4: uuidv4 } = require('uuid');
const asyncLocalStorage = new AsyncLocalStorage();

// Middleware - extract or generate correlation ID
function correlationMiddleware(req, res, next) {
  const correlationId = req.headers['x-correlation-id'] || uuidv4();

  // Store in AsyncLocalStorage
  asyncLocalStorage.run({ correlationId }, () => {
    res.setHeader('x-correlation-id', correlationId);
    next();
  });
}

// Get correlation ID anywhere
function getCorrelationId() {
  return asyncLocalStorage.getStore()?.correlationId;
}

// Axios interceptor - propagate to other services
axios.interceptors.request.use((config) => {
  const correlationId = getCorrelationId();
  if (correlationId) {
    config.headers['x-correlation-id'] = correlationId;
  }
  return config;
});

// Logger with correlation ID
const logger = {
  info: (message, data) => {
    console.log(JSON.stringify({
      level: 'info',
      correlationId: getCorrelationId(),
      message,
      ...data,
      timestamp: new Date().toISOString()
    }));
  }
};

Health Checks

const express = require('express');
const app = express();

// Liveness - czy proces żyje
app.get('/health/live', (req, res) => {
  res.status(200).json({ status: 'alive' });
});

// Readiness - czy może obsługiwać ruch
app.get('/health/ready', async (req, res) => {
  const checks = {
    database: await checkDatabase(),
    redis: await checkRedis(),
    rabbitmq: await checkRabbitMQ()
  };

  const allHealthy = Object.values(checks).every(c => c.status === 'healthy');

  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? 'ready' : 'not_ready',
    checks
  });
});

async function checkDatabase() {
  try {
    await mongoose.connection.db.admin().ping();
    return { status: 'healthy' };
  } catch (error) {
    return { status: 'unhealthy', error: error.message };
  }
}

Microservices Checklist

Architektura

  • Jasno zdefiniowane bounded contexts
  • Database per service
  • Niezależne deployments
  • API contracts (OpenAPI, protobuf)

Komunikacja

  • Synchroniczna (REST/gRPC) dla queries
  • Asynchroniczna (events) dla commands
  • Circuit breaker dla wywołań zewnętrznych
  • Retry z exponential backoff

Resilience

  • Circuit breaker
  • Bulkhead (izolacja zasobów)
  • Timeouts na każdym wywołaniu
  • Fallback responses
  • Graceful degradation

Data

  • Eventual consistency strategy
  • Saga dla transakcji rozproszonych
  • Outbox pattern
  • Idempotent consumers

Observability

  • Distributed tracing (Jaeger/Zipkin)
  • Correlation ID propagation
  • Centralized logging
  • Health checks (liveness/readiness)
  • Metrics (latency, throughput, errors)

Powiązane Artykuły


Chcesz Więcej Pytań o Microservices?

Mamy 50 pytań rekrutacyjnych o microservices w Node.js - od podstaw przez RabbitMQ, Kafka, wzorce, po monitoring. Każde z pełną odpowiedzią.

Zdobądź Pełny Dostęp do Wszystkich Pytań


Napisane przez zespół Flipcards, na podstawie doświadczeń z budowania systemów rozproszonych w Node.js.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.