بناء واجهات API مرنة: Retry و Circuit Breaker و Rate Limiting
مقال عملي من منظور Full-Stack Web Developer عن كيفية بناء APIs تتحمل أعطال الإنتاج باستخدام timeouts و retries و circuit breakers و rate limiting، مع أمثلة في Node.js و NestJS و Go.
# بناء واجهات API مرنة: Retry و Circuit Breaker و Rate Limiting
مرحباً، اسمي عمرو سمير، وأنا Full-Stack Web Developer.
يمكن نعم، ويمكن لا، لكن أعتقد أن كل مطوّر يصل في مرحلة معيّنة إلى نقطة مهمة: لم يعد السؤال فقط هو: “كيف أجعل الـ API يعمل؟” بل يصبح السؤال الأهم:
“ماذا سيحدث عندما يفشل هذا الـ API؟”
لأن الحقيقة البسيطة هي: أي نظام حقيقي سيفشل في وقت ما.
قد تكون قاعدة البيانات بطيئة. قد تتوقف خدمة خارجية لدقائق. قد تحدث مشكلة في الشبكة بدون سبب واضح. قد يأتي ضغط كبير على النظام في أسوأ وقت ممكن. كل هذا طبيعي جداً في بيئة الإنتاج، خصوصاً عندما نبني تطبيقات تعتمد على أكثر من خدمة، أو Microservices، أو Payment Flow، أو Dashboards، أو SaaS Products.
في هذا المقال، أريد أن أتكلم عن API Resilience بطريقة عملية وبسيطة:
Timeouts, Retries, Circuit Breakers, Rate Limiting, Security, Testing, and Incident Response.
ليس ككلام نظري جميل، لكن كأدوات حقيقية تساعد التطبيق أن يظل واقفاً عندما يبدأ الإنتاج في التصرف كالإنتاج فعلاً.
لماذا نحتاج إلى API Resilience؟
عندما يكون كل شيء سليماً، تبدو الـ APIs بسيطة جداً.
الـ Client يرسل Request. الـ Backend يستقبله. الـ Backend يتعامل مع Database أو Service أخرى. ثم يرجع Response. الموضوع يبدو نظيفاً وسلساً.
لكن الإنتاج ليس دائماً بهذا الهدوء.
خدمة Downstream قد تصبح بطيئة. Request قد يظل معلقاً. Client قد يعيد المحاولة أكثر من اللازم. Queue قد تمتلئ. CPU قد يرتفع. Memory قد تزيد. وفجأة، مشكلة صغيرة في خدمة واحدة تتحول إلى Outage أكبر في النظام كله.
وهنا تظهر أهمية المرونة.
الـ API المرن ليس API لا يفشل أبداً. هذا غير واقعي. الـ API المرن هو API يعرف كيف يفشل بطريقة واضحة ومحدودة وآمنة.
بمعنى أنه يجب أن:
- لا ينتظر للأبد.
- يعيد المحاولة بحذر.
- يتجنب Retry Storms.
- يتوقف مؤقتاً عن استدعاء الخدمات غير الصحية.
- يرفض الضغط الزائد مبكراً.
- يحمي باقي النظام من الانهيار.
- يتعافى بدون Panic أو Restart عشوائي.
بالنسبة لي، Resilience معناها أننا لا نبني النظام فقط لأيام العرض والـ Demo، بل نبنيه أيضاً للأيام السيئة.
شكل المشكلة في الأنظمة الحقيقية
معظم مشاكل الإنتاج تتبع نمطاً متكرراً.
خدمة معينة تصبح بطيئة. الخدمة التي تنادي عليها ليس لديها Timeout مناسب. الـ Requests تبدأ في الانتظار. تأتي Requests أكثر. بعض العملاء يعيدون المحاولة. الـ Retries تضيف ضغطاً أكبر. الخدمة البطيئة تصبح أبطأ. وبعدها تبدأ خدمات أخرى في التأثر.
المشكلة الأصلية قد تكون صغيرة، لكن طريقة تعامل النظام معها تجعلها أكبر.
لذلك أرى أن الأنماط التالية ليست إضافات اختيارية:
- Timeouts
- Retries with Backoff and Jitter
- Circuit Breakers
- Rate Limiting
- Fallbacks
- Observability
هذه ليست “Nice to have”. هذه أدوات أمان للإنتاج.
ابدأ دائماً بالـ Timeouts
قبل أن نتكلم عن Retry أو Circuit Breaker، يجب أن نبدأ بأبسط قاعدة:
أي استدعاء خارجي يجب أن يكون له Timeout.
لا يجب أن ينتظر الـ API للأبد حتى ترد خدمة أخرى.
هذا يشمل:
- HTTP Requests
- Database Queries
- Redis / Cache Calls
- Message Broker Operations
- File Storage Calls
- Third-Party API Calls
بدون Timeout، يمكن أن يظل Request واحد معلقاً لفترة طويلة. ومع عدد كبير من الطلبات، يتحول الأمر إلى استهلاك Threads و Memory و Connections بدون فائدة.
الـ Timeout يضع حدوداً واضحة. هو يقول: “سأنتظر هذه المدة فقط، ثم أتوقف.”
هذا القرار بسيط، لكنه من أهم قرارات بناء أنظمة مستقرة.
Retry Pattern: إعادة المحاولة لكن بحذر
الـ Retry يعني ببساطة أن نعيد تنفيذ نفس الطلب بعد فشله.
هذا مفيد لأن ليس كل فشل دائم. أحياناً الشبكة يحدث بها Glitch. أحياناً خدمة تكون مضغوطة لثوانٍ. أحياناً API خارجي يرجع 503 ثم يعود للعمل.
في هذه الحالات، Retry قد يحسن تجربة المستخدم.
لكن المشكلة تبدأ عندما نعيد المحاولة بدون تفكير.
إذا كانت الخدمة أصلاً تحت ضغط، وإرسال Requests أكثر سيزيد الضغط، فالـ Retry هنا لن يحل المشكلة. بالعكس، قد يحولها إلى Retry Storm.
متى نستخدم Retry؟
استخدم Retry مع الأخطاء المؤقتة مثل:
- Network Timeouts
- Connection Resets
503 Service Unavailable502 Bad Gateway504 Gateway Timeout- أحياناً
429 Too Many Requestsإذا كان الـ Server يوضح متى يمكن إعادة المحاولة.
ولا تستخدم Retry عادةً مع:
400 Bad Request401 Unauthorized403 Forbidden- Validation Errors
- Business Logic Errors
- العمليات غير الآمنة بدون Idempotency
النقطة الأخيرة مهمة جداً. إذا أعدت تنفيذ عملية Payment أو Order Creation أو Money Transfer بدون Idempotency Key، قد تنفذ العملية مرتين.
وهذا ليس Resilience. هذا Bug جديد.
Exponential Backoff و Jitter
أسوأ طريقة للـ Retry هي أن تعيد المحاولة فوراً.
الأفضل هو استخدام Exponential Backoff. مثال بسيط:
Attempt 1: wait 100ms
Attempt 2: wait 200ms
Attempt 3: wait 400ms
Attempt 4: wait 800ms
ثم نضيف Jitter، أي تأخير عشوائي بسيط.
لماذا؟ لأننا لا نريد كل الـ Clients يعيدون المحاولة في نفس اللحظة. الـ Jitter يجعل المحاولات موزعة على وقت أطول، وهذا يساعد الخدمة على التعافي.
قاعدتي الشخصية:
Retry slowly, retry a few times, and never retry forever.
مثال Retry في Node.js
مثال بسيط باستخدام axios-retry:
const axios = require('axios');
const axiosRetry = require('axios-retry');
axiosRetry(axios, {
retries: 3,
retryCondition: (error) => {
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
},
retryDelay: axiosRetry.exponentialDelay,
});
async function getData() {
try {
const response = await axios.get('https://api.example.com/data', {
timeout: 2000,
});
return response.data;
} catch (error) {
console.error('Request failed after retries:', error.message);
throw error;
}
}
المهم هنا ليس فقط أننا استخدمنا Retry. المهم أننا:
- نعيد المحاولة فقط مع الأخطاء المناسبة.
- نحدد عدد المحاولات.
- نستخدم Delay مناسب.
- نضع Timeout.
- نسجل الفشل النهائي بوضوح.
مثال Retry في NestJS
في NestJS يمكن استخدام RxJS مع HttpService:
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
@Injectable()
export class ApiClientService {
constructor(private readonly httpService: HttpService) {}
async getData() {
return firstValueFrom(
this.httpService.get('https://api.example.com/data').pipe(
retry({
count: 3,
delay: (_error, attempt) => 100 * Math.pow(2, attempt),
}),
catchError((error) => {
console.error('Request failed after retries:', error.message);
return throwError(() => error);
}),
),
);
}
}
في المشاريع الحقيقية، أفضل أن تكون إعدادات Retry قابلة للتغيير من Config، لأن القيم التي تصلح اليوم قد لا تصلح بعد زيادة الضغط أو تغيير نوع الخدمة.
مثال Retry في Go
في Go يمكن استخدام go-retryablehttp:
package main
import (
"log"
"time"
"github.com/hashicorp/go-retryablehttp"
)
func main() {
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 100 * time.Millisecond
client.RetryWaitMax = 1 * time.Second
resp, err := client.Get("https://api.example.com/data")
if err != nil {
log.Fatalf("request failed after retries: %v", err)
}
defer resp.Body.Close()
log.Println("request completed with status:", resp.Status)
}
في Production، سأضيف أيضاً Context Cancellation و HTTP Client Timeout و Structured Logging.
Circuit Breaker Pattern
الـ Circuit Breaker يشبه فكرة الفيوز الكهربائي.
عندما تبدأ خدمة Downstream في الفشل بشكل متكرر، يقوم الـ Circuit Breaker بفتح الدائرة، أي يتوقف مؤقتاً عن إرسال Requests لهذه الخدمة. بدلاً من أن ينتظر كل Request حتى يفشل بالـ Timeout، يفشل بسرعة وبشكل واضح.
قد يبدو ذلك قاسياً، لكنه غالباً القرار الصحيح.
إذا كانت خدمة Payment أو User Service أو Search Service أو Database في حالة غير صحية، فاستمرار الضغط عليها سيجعل التعافي أصعب. الـ Circuit Breaker يعطي الخدمة فرصة للتنفس ويحمي باقي النظام.
Closed
هذه هي الحالة الطبيعية. الطلبات تمر للخدمة بشكل عادي. الـ Breaker يراقب الأخطاء والـ Timeouts.
إذا تجاوزت الأخطاء Threshold معين، ينتقل إلى Open.
Open
في هذه الحالة لا يرسل Requests للخدمة. الطلب يفشل مباشرة أو يرجع Fallback Response.
هذا يحمي الخدمة المتعبة ويحمي التطبيق من الانتظار الطويل.
Half-Open
بعد فترة انتظار، يسمح الـ Breaker بعدد قليل من الطلبات التجريبية.
إذا نجحت، يعود إلى Closed. إذا فشلت، يرجع إلى Open.
هذه الحالة مهمة لأنها تمنع النظام من إرسال كامل الضغط مرة واحدة بعد التعافي.
مثال Circuit Breaker في Node.js
مثال باستخدام opossum:
const CircuitBreaker = require('opossum');
const axios = require('axios');
async function fetchData() {
const response = await axios.get('https://api.example.com/data', {
timeout: 3000,
});
return response.data;
}
const breaker = new CircuitBreaker(fetchData, {
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 10000,
});
breaker.fallback(() => {
return {
message: 'Service temporarily unavailable',
degraded: true,
};
});
async function getProtectedData() {
try {
return await breaker.fire();
} catch (error) {
console.error('Circuit breaker request failed:', error.message);
throw error;
}
}
الـ Fallback ليس دائماً مطلوباً، لكنه مفيد إذا كان يمكنك إرجاع Cached Data أو رسالة مفهومة للمستخدم بدلاً من Error خام.
مثال Circuit Breaker في Go
مثال باستخدام sony/gobreaker:
package main
import (
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/sony/gobreaker"
)
func main() {
settings := gobreaker.Settings{
Name: "PaymentServiceBreaker",
MaxRequests: 3,
Interval: 60 * time.Second,
Timeout: 10 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
},
}
breaker := gobreaker.NewCircuitBreaker(settings)
r := gin.Default()
r.GET("/order", func(c *gin.Context) {
result, err := breaker.Execute(func() (interface{}, error) {
client := &http.Client{ Timeout: 2 * time.Second }
resp, err := client.Get("https://payment.example.com/pay")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("payment service returned non-200 status")
}
return "payment ok", nil
})
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Payment service temporarily unavailable",
})
return
}
c.JSON(http.StatusOK, gin.H{ "result": result })
})
r.Run(":8080")
}
الفكرة هنا واضحة: لا تجعل كل Request ينتظر خدمة قد تكون واقعة بالفعل.
تحديات Circuit Breakers
الـ Circuit Breaker قوي، لكنه يحتاج إلى ضبط جيد.
إذا فتح بسرعة كبيرة، قد تمنع طلبات كان يمكن أن تنجح. إذا فتح متأخراً، لن يحمي النظام في الوقت المناسب.
لذلك يجب التفكير في:
- Failure Threshold
- Timeout Values
- Cooldown Period
- Half-Open Requests
- Fallback Behavior
- Metrics and Alerts
أيضاً، يجب أن تعرف متى فتح الـ Breaker ولماذا، ومتى أغلق مرة أخرى. بدون Observability، أنت تعمل في الظلام.
Rate Limiting و Throttling
Rate Limiting يعني تحديد عدد الطلبات التي يمكن لعميل أو مستخدم أو IP أو API Key إرسالها خلال فترة زمنية معينة.
هذا من أكثر الأشياء العملية التي تحمي الـ API.
بدون Rate Limit، يمكن لعميل واحد، أو Bot، أو Bug في Client، أو Spike مفاجئ أن يستهلك موارد النظام كلها.
عادةً يكون Rate Limit مكوّناً من:
- Limit: عدد الطلبات المسموح بها.
- Window: الفترة الزمنية.
- Identifier: من نطبق عليه الحد، مثل User ID أو API Key أو IP.
مثال:
100 requests per minute per user
1000 requests per hour per API key
10 login attempts per 5 minutes per IP
عندما يتجاوز العميل الحد، نرجع غالباً:
HTTP 429 Too Many Requests
ومن الأفضل إضافة Header مثل Retry-After لتوضيح متى يمكنه المحاولة مرة أخرى.
أشهر خوارزميات Rate Limiting
Fixed Window
نحسب عدد الطلبات في نافذة زمنية ثابتة، مثل دقيقة واحدة. إذا وصل المستخدم للحد، نرفض الطلبات حتى تبدأ النافذة التالية.
سهل، لكنه قد يسمح ببعض الـ Bursts عند بداية ونهاية النافذة.
Sliding Window
يعطي قياساً أنعم لحركة الطلبات خلال الوقت. أدق من Fixed Window، لكنه أكثر تكلفة في التنفيذ.
Token Bucket
كل Request يحتاج Token. الـ Tokens يتم ملؤها مع الوقت. إذا كانت هناك Tokens متاحة، يمر الطلب. إذا لم توجد، نرفض الطلب.
هذا الأسلوب عملي جداً لأنه يسمح ببعض الـ Bursts الصغيرة، مع الحفاظ على متوسط ثابت.
Leaky Bucket
يعالج الطلبات بمعدل ثابت. أي ضغط زائد يتم تأخيره أو رفضه.
مفيد عندما تريد جعل الضغط الخارج إلى خدمة معينة ثابتاً قدر الإمكان.
مثال Rate Limiting في Express
مثال باستخدام express-rate-limit:
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: {
error: 'Too many requests. Please try again later.',
},
});
app.use('/api/', apiLimiter);
app.get('/api/data', (req, res) => {
res.json({ message: 'ok' });
});
app.listen(3000);
في Production، انتبه إذا كان التطبيق خلف Proxy أو Load Balancer، لأنك تحتاج غالباً إلى ضبط Trust Proxy حتى تحصل على IP الحقيقي للعميل.
مثال Rate Limiting في NestJS
في NestJS يمكن استخدام @nestjs/throttler:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
@UseGuards(ThrottlerGuard)
@Controller('messages')
export class MessagesController {
@Get()
@Throttle({ default: { limit: 10, ttl: 60000 } })
getMessages() {
return { data: [] };
}
}
في الأنظمة الحقيقية، لا أحب استخدام نفس الحد لكل Endpoints. Login و Search و Export و Public APIs كل واحد منهم يحتاج سياسة مختلفة.
مثال Rate Limiting في Go مع Gin
مثال بسيط باستخدام Token Bucket داخل الذاكرة:
package main
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
var (
visitors = make(map[string]*rate.Limiter)
mu sync.Mutex
)
func getLimiter(ip string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()
limiter, exists := visitors[ip]
if exists {
return limiter
}
limiter = rate.NewLimiter(5, 10)
visitors[ip] = limiter
return limiter
}
func rateLimiterMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
limiter := getLimiter(ip)
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
})
return
}
c.Next()
}
}
func main() {
r := gin.Default()
r.Use(rateLimiterMiddleware())
r.GET("/api/data", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ "message": "ok" })
})
r.Run(":8080")
}
هذا المثال جيد للتعلم، لكنه ليس مناسباً وحده لتطبيق يعمل على أكثر من Instance، لأن كل Instance سيكون لديها Memory خاصة بها. في Production، الأفضل استخدام Redis أو API Gateway أو Rate Limiting Service مشتركة.
Security جزء من Resilience
أحياناً نتكلم عن Security و Resilience كأنهما موضوعان منفصلان، لكن في الواقع بينهما علاقة قوية.
API غير آمن يمكن أن يصبح غير متاح بسهولة. Brute Force، Tokens مسربة، Missing Authorization، Injection Attacks، أو Abuse قد تؤدي كلها إلى مشاكل Availability.
لذلك، أي API مرن يجب أن يكون آمناً أيضاً.
أهم الأساسيات:
- استخدم HTTPS دائماً.
- طبّق Authentication قوي.
- طبّق Authorization على مستوى الموارد، وليس فقط تسجيل الدخول.
- تحقق من كل Input.
- لا تضع Secrets داخل الكود.
- استخدم Least Privilege.
- راقب Security Events.
- ضع Rate Limits على Endpoints الحساسة مثل Login و Password Reset.
في NestJS مثلاً، يمكن استخدام Pipes و class-validator. في Node.js يمكن استخدام Zod أو Joi أو Ajv. في Go يمكن استخدام Struct Validation.
الفكرة العامة: لا تثق في أي Input قادم من الخارج.
Testing: لا تنتظر الإنتاج ليخبرك بالحقيقة
Resilience لا يجب أن تكون مجرد كلام في Architecture Document.
يجب أن تختبرها.
اختبر ماذا يحدث عندما:
- خدمة خارجية ترجع 500.
- الخدمة تصبح بطيئة.
- قاعدة البيانات لا ترد.
- API خارجي يرجع 429.
- الشبكة تفشل.
- الضغط يزيد فجأة.
- Queue تمتلئ.
- Circuit Breaker يفتح.
- Rate Limit يبدأ في رفض الطلبات.
Unit Tests
اختبر Retry Logic و Timeout Handling و Fallback Responses و Circuit Breaker Behavior.
Integration Tests
اختبر كيف تتعامل الخدمات مع بعضها عند الفشل.
مثلاً: ماذا يحدث إذا كانت User Service بطيئة؟ هل Order Service تفشل بسرعة؟ هل Retry محدود؟ هل Circuit Breaker يفتح؟
Load Testing
استخدم أدوات مثل:
- k6
- Artillery
- Locust
- JMeter
لا تختبر فقط Happy Path. اختبر الضغط، الـ Spikes، والطلبات غير المتساوية.
Chaos Testing
Chaos Testing يعني أن تضيف فشل متعمد لترى كيف يتصرف النظام.
مثلاً:
- أوقف Container.
- أضف Latency بين خدمات.
- اجعل Dependency يرجع 500.
- أوقف Cache.
- أضف Packet Loss.
الهدف ليس كسر النظام من أجل المتعة. الهدف أن تعرف كيف سيتصرف قبل أن يحدث ذلك فعلاً في Production.
Observability: لا يمكنك إصلاح ما لا تراه
أي نظام مرن يحتاج إلى Metrics و Logs و Traces واضحة.
راقب على الأقل:
- Request Count
- Latency
- Error Rate
- Timeout Count
- Retry Count
- Circuit Breaker State Changes
- Rate Limit Hits
- HTTP 429 Responses
- Queue Depth
- CPU and Memory
- Dependency Health
وأضف Correlation ID أو Request ID حتى تستطيع تتبع الطلب بين الخدمات.
في وقت الحوادث، Dashboard جيد يوفر الكثير من الوقت والتوتر.
ماذا تفعل وقت Incident؟
عندما يبدأ Incident، أول هدف ليس كتابة حل مثالي. أول هدف هو تقليل الضرر.
بعض الخطوات السريعة:
- قلل Rate Limits على الـ Endpoints المتأثرة.
- أوقف Features غير مهمة مؤقتاً.
- أوقف Retries العدوانية.
- افتح Circuit يدوياً إذا كان النظام يسمح بذلك.
- اعمل Rollback إذا كان السبب Deploy جديد.
- أوقف Background Jobs غير ضرورية.
- قلل Queue Processing.
- استخدم Cached Responses إن أمكن.
- أظهر Degraded Experience بدلاً من انهيار كامل.
بعد الاستقرار، ابدأ التحليل:
- ما آخر Deploy؟
- ما التغيير في Config؟
- أي Dependency فشلت؟
- هل زاد Traffic؟
- هل حدث Retry Storm؟
- هل كانت هناك Alerts واضحة؟
ثم اكتب Postmortem بدون لوم، الهدف منه أن نفس المشكلة لا تتكرر بنفس الشكل.
ماذا يحدث إذا تجاهلت Resilience؟
في البداية، قد يبدو تجاهل هذه الأشياء مقبولاً.
الضغط قليل. الخدمات تعمل. الفريق يريد بناء Features بسرعة. لا أحد يريد قضاء وقت في Timeouts و Circuit Breakers و Rate Limits.
لكن عندما يكبر النظام، تظهر تكلفة هذه القرارات:
- Requests تنتظر أكثر من اللازم.
- Clients تعيد المحاولة بشكل مؤذٍ.
- Dependency واحدة تسحب النظام كله معها.
- Queues تصبح غير قابلة للتحكم.
- لا أحد يعرف أين المشكلة.
- المستخدم يرى بطئاً أو فشلاً كاملاً.
- الفريق يدخل في Panic لأن لا توجد Runbooks.
لذلك لا أرى Resilience كشيء نضيفه بعد أن نفشل. الأفضل أن نبدأ به مبكراً ولو بشكل بسيط.
ابدأ بـ Timeouts. ثم Retries محدودة. ثم Rate Limits أساسية. ثم Circuit Breakers للخدمات المهمة. وبعدها حسّن تدريجياً.
Roadmap عملي لبناء API أكثر مرونة
لو كنت أعمل على API وأريد تحسينها، سأبدأ بهذا الترتيب:
-
ضع Timeouts لكل Outbound Call
HTTP، Database، Cache، Message Broker، وأي خدمة خارجية. -
أضف Retries محدودة للأخطاء المؤقتة فقط
استخدم Backoff و Jitter ولا تعيد المحاولة إلى ما لا نهاية. -
استخدم Idempotency للعمليات الحساسة
خصوصاً Payments، Orders، Transfers، وأي عملية قد تسبب Side Effect. -
أضف Circuit Breakers للخدمات المهمة
مثل Payment Provider، User Service، Search Service، External APIs. -
طبّق Rate Limiting
ابدأ بـ Login، Search، Signup، Password Reset، Public APIs، والـ Endpoints المكلفة. -
حسّن Security
HTTPS، Auth، Authorization، Input Validation، Secrets Management، Least Privilege. -
أضف Observability
Metrics، Logs، Traces، Alerts، Dashboards. -
اختبر الفشل وليس النجاح فقط
Simulate timeouts, 500s, 429s, latency, traffic spikes, and queue overload. -
اكتب Runbooks
عندما يحدث Alert، يجب أن يعرف الفريق ماذا يفعل بسرعة. -
راجع الإعدادات باستمرار
Traffic يتغير، Dependencies تتغير، والنظام نفسه يتطور.
الخلاصة
بناء API مرنة لا يعني أن الفشل لن يحدث. الفشل سيحدث، وهذه طبيعة الأنظمة الموزعة.
الفكرة هي أن نجعل الفشل محدوداً وواضحاً وقابلاً للتعافي.
- الـ Timeouts تمنع الانتظار للأبد.
- الـ Retries تساعد في الأخطاء المؤقتة، بشرط استخدامها بحذر.
- الـ Circuit Breakers تمنع خدمة متعبة من إسقاط النظام كله.
- الـ Rate Limiting يحمي الموارد من الضغط الزائد أو الاستخدام السيئ.
- الـ Security يحمي النظام من نوع آخر من الأعطال.
- الـ Testing and Observability يجعلانك تعرف هل هذه الحماية تعمل فعلاً أم لا.
ابدأ صغيراً. لا تحتاج إلى منصة مثالية من اليوم الأول.
لكن لا تترك الـ API بدون Timeouts. لا تجعل Retries مفتوحة. لا تسمح لأي Client أن يستهلك النظام كله. ولا تنتظر أول Incident كبير حتى تبدأ التفكير في Resilience.
بالنسبة لي، هذه ليست تفاصيل Backend فقط. هذه طريقة تفكير في بناء برامج حقيقية تعيش في Production.
منشورات مقترحة
مشاريع ذات صلة

E-techPay هو منصة تجارة إلكترونية كاملة تجعل التسوق عبر الإنترنت سهلًا وآمنًا. إنه سريع، موثوق، ومصمم لتقديم أفضل تجربة تسوق.
