50+ Microservices Node.js Pytania Rekrutacyjne 2026: RabbitMQ, Kafka i Saga
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
- Podstawy Microservices Pytania
- Komunikacja między Serwisami Pytania
- Message Brokers RabbitMQ vs Kafka Pytania
- API Gateway Pytania
- Wzorce Microservices Pytania
- Event-Driven Architecture Pytania
- Database per Service Pytania
- Monitoring i Observability Pytania
- 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
- Kompletny Przewodnik - Rozmowa Node.js Backend Developer - pełna mapa wiedzy backend
- REST API Best Practices - projektowanie API
- Wzorce i Architektura Backend - wzorce projektowe
- Docker Pytania Rekrutacyjne - konteneryzacja microservices
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.
