Transactional Outbox و Inbox: الـ event ما يضيعش وما يتنفذش مرتين
بنحفظ الـ order وبعدين ننشر الـ event، والـ process تقع في النص، فالـ order موجود والـ billing عمره ما عرف. ولو نشرنا الأول، ممكن الـ event يوصل والحفظ يفشل. والـ broker ممكن يستلم والـ ack يضيع، فالـ message تتبعت مرتين والعميل يتحاسب مرتين. بنفهم الـ dual write، والـ outbox جُوّه نفس الـ transaction، والـ relay بـ FOR UPDATE SKIP LOCKED والإعادة والـ dead messages، والترتيب لكل aggregate، والـ at least once، والـ inbox اللي بيخلّي الـ consumer idempotent، والـ CDC كبديل.
نص شرح المعلم والكود الذي كتبه ونتيجة تشغيله، بنفس ترتيب الدرس.
أهلًا بيك. في الدرس اللي فات، الـ orders كان بيحفظ، وبعدين بيعمل publish للـ event. وقلنا فيه مشكلة. النهارده هنحلّها. بص على orders dot naive dot js. الطريقة دي اسمها dual write: بنكتب في مكانين، الـ database والـ broker، ومفيش حاجة بتربطهم ببعض. الـ database transaction ما تعرفش حاجة عن الـ broker، والـ broker ما يعرفش حاجة عن الـ database. إيه اللي ممكن يحصل؟ الـ INSERT نجح، والـ process اتقفلت قبل الـ publish. deploy، أو OOM، أو الـ server اتعمله restart. الـ order موجود، والـ event ضاع للأبد. الـ broker واقع خمس دقايق. كل الـ orders في الخمس دقايق دول من غير events. لو عكست، publish الأول: الـ event وصل، والـ INSERT فشل. دلوقتي الـ billing بيعمل invoice لـ order مش موجود. ومشكلة تانية خالص في الـ consumer: الـ broker ممكن يبعت نفس الـ message مرتين. ودي مش bug، ده الطبيعي. ولو الـ consumer بيعمل INSERT لكل message، العميل هيتحاسب مرتين. والحل الأكيد للمشكلتين اسمه outbox و inbox.
الفكرة بسيطة أوي: الـ database هي الحاجة الوحيدة اللي عندها transaction. فخلّي الـ event يتكتب في الـ database، في table اسمها outbox، جُوّه نفس الـ transaction بتاعة الـ order. يا الاتنين يتكتبوا، يا ولا واحد. وبعدين process تانية اسمها relay، بتقرا من الـ outbox وتنشر للـ broker، وتعلّم على اللي اتنشر. ولو الـ broker واقع، الـ events مستنية في الـ table، ولا حاجة ضاعت. بص على schema dot sql. الـ outbox فيه aggregate_id، يعني الـ order ده بتاع مين، و type و payload كـ jsonb. published_at بيبقى null لحد ما يتنشر. attempts و last_error للإعادة والـ debugging. والـ index partial: بس على اللي لسه ما اتنشرش، فبيفضل صغير حتى لو الـ table فيها ملايين rows منشورة. والـ inbox: الـ primary key هو الـ consumer مع الـ message id. والـ invoices من غير unique على order_id عن قصد، علشان نشوف الـ inbox بيشتغل لوحده.
الـ relay، وفيه تفاصيل مهمة. SKIP LOCKED هو اللي بيخلّيك تشغّل أكتر من relay، علشان الـ availability. كل واحد بيقفل الـ rows اللي سحبها، والتاني بيعدّيها ويسحب اللي بعدها، من غير ما يستنى. الترتيب: لو OrderPlaced فشل و OrderPaid بعده نجح، الـ consumer هيشوف paid لـ order ما يعرفوش. علشان كده لو message فشلت، أي message بعدها لنفس الـ aggregate تستنى. والـ orders التانية تكمّل عادي، علشان order واحد ما يوقفش الكل. ولو عندك أكتر من relay، الترتيب الكامل محتاج partitioning بالـ aggregate، وده هنشوفه مع Kafka. وأهم حاجة: الـ relay ممكن ينشر وبعدين يقع قبل ما يكتب published_at. أو الـ broker استلم، والـ ack ضاع. في الحالتين، هينشر تاني. يعني الـ outbox بيدّيك at least once، مش exactly once. ودي مش مشكلة في الـ outbox، دي حقيقة في أي distributed system. والحل عند الـ consumer.
الـ inbox. الـ consumer قبل ما يشتغل على أي message، بيعمل INSERT للـ message id في الـ inbox، جُوّه نفس الـ transaction بتاعة الشغل نفسه. لو الـ INSERT عمل conflict، يبقى الـ message دي اتعملت قبل كده، فيعدّيها. ولو الشغل نفسه فشل، الـ transaction كلها بترجع، ومعاها الـ inbox row، فالمرة الجاية يحاول تاني. at least once من الـ relay، زائد idempotent consumer، بيدّيك effectively once. ده أقرب حاجة لـ exactly once في الحقيقة. وكام حاجة للـ production: الـ outbox بيكبر. اعمل cleanup للمنشور الأقدم من أسبوع، بس عمرك ما تمسح pending. راقب الـ pending count وعمر أقدم message. لو العمر بيزيد، الـ relay واقف. ده alert مهم. والـ dead messages محتاجة حد يبصلها، مش retry للأبد. وفيه بديل للـ polling اسمه CDC، Change Data Capture، زي Debezium. بيقرا الـ WAL بتاع Postgres نفسه، ويطلّع كل INSERT في الـ outbox كـ event، من غير queries. أسرع وأخف على الـ database، بس infrastructure زيادة. ابدأ بالـ polling، وانقل لـ CDC لما تحتاج.
التحدي. عندك lab dot js، بيشغّل كل السيناريوهات على Postgres حقيقي: الـ dual write وهو بيضيّع events، وبعدين الـ outbox، و broker واقع و acks ضايعة، والترتيب، واتنين relays مع بعض. والـ broker نفسه array في الـ memory، علشان نركّز على الـ database. والمطلوب outbox dot js. كل التفاصيل في الكومنت: withTransaction، و enqueue و placeOrder، و relayOnce بالـ SKIP LOCKED والترتيب، و handleOnce، و pendingStats و cleanup. اقرا الكومنت كويس، وخصوصًا الـ relayOnce، ونشغّل test dot js لحد ما تبقى أربعتاشر من أربعتاشر.
اكتب outbox.js: withTransaction، و enqueue، و placeOrder، و relayOnce بـ FOR UPDATE SKIP LOCKED والإعادة والترتيب لكل aggregate، و handleOnce بالـ inbox، و pendingStats، و cleanup. كل التفاصيل في الكومنت، وشغّل node test.js.
الحل. withTransaction: الـ finally هو اللي بيضمن إن الـ client بيرجع للـ pool حتى لو حصل error. enqueue بياخد client مش pool، وده مقصود: كده مستحيل تكتب في الـ outbox بره الـ transaction بتاعة الـ business. relayOnce: الـ SELECT والـ UPDATEs كلهم في transaction واحدة، فالـ locks فاضلة لحد الـ COMMIT. و Set للـ aggregates اللي فشلت. handleOnce: الـ rowCount بتاع الـ INSERT هو اللي بيقول الـ message جديدة ولا لأ. نشغّل lab dot js.
// outbox.js: الـ transactional outbox والـ relay، والـ inbox للـ consumer، والـ stats والـ cleanup
export async function withTransaction(pool, fn) {
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (e) {
await client.query("ROLLBACK").catch(() => {});
throw e;
} finally {
client.release();
}
}
export async function enqueue(client, { aggregateId, type, payload }) {
const { rows } = await client.query(
"INSERT INTO outbox (aggregate_id, type, payload) VALUES ($1, $2, $3) RETURNING id",
[aggregateId, type, JSON.stringify(payload)],
);
return String(rows[0].id);
}
export function placeOrder(pool, { id, customer, totalMinor }) {
return withTransaction(pool, async (c) => {
await c.query("INSERT INTO orders (id, customer, total_minor) VALUES ($1, $2, $3)", [id, customer, totalMinor]);
return enqueue(c, { aggregateId: id, type: "OrderPlaced", payload: { orderId: id, customer, totalMinor } });
});
}
export function relayOnce(pool, publish, { batchSize = 10, maxAttempts = 5 } = {}) {
return withTransaction(pool, async (c) => {
const { rows } = await c.query(
`SELECT id, aggregate_id, type, payload FROM outbox
WHERE published_at IS NULL AND attempts < $1
ORDER BY id LIMIT $2 FOR UPDATE SKIP LOCKED`,
[maxAttempts, batchSize],
);
const blocked = new Set();
const stats = { published: 0, failed: 0, skipped: 0 };
for (const r of rows) {
if (blocked.has(r.aggregate_id)) { stats.skipped++; continue; }
try {
await publish({ id: String(r.id), aggregateId: r.aggregate_id, type: r.type, payload: r.payload });
await c.query("UPDATE outbox SET published_at = now() WHERE id = $1", [r.id]);
stats.published++;
} catch (e) {
await c.query("UPDATE outbox SET attempts = attempts + 1, last_error = $2 WHERE id = $1", [r.id, String(e.message)]);
blocked.add(r.aggregate_id);
stats.failed++;
}
}
return stats;
});
}
export function handleOnce(pool, consumer, message, effect) {
return withTransaction(pool, async (c) => {
const { rowCount } = await c.query(
"INSERT INTO inbox (consumer, message_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
[consumer, message.id],
);
if (!rowCount) return "duplicate";
await effect(c, message);
return "processed";
});
}
export async function pendingStats(pool, { maxAttempts = 5 } = {}) {
const { rows } = await pool.query(
`SELECT count(*) FILTER (WHERE attempts < $1) AS pending,
count(*) FILTER (WHERE attempts >= $1) AS dead,
floor(extract(epoch FROM now() - min(created_at) FILTER (WHERE attempts < $1))) AS oldest
FROM outbox WHERE published_at IS NULL`,
[maxAttempts],
);
const r = rows[0];
return { pending: Number(r.pending), dead: Number(r.dead), oldestPendingSecon
…المخرجات
$ node lab.js
① dual write: احفظ وبعدين انشر، والـ process بتقع في النص:
الـ process اتقفلت
orders: 1 و events في الـ broker: 0 ← الـ billing عمره ما هيعرف
② انشر الأول وبعدين احفظ، والحفظ بيفشل:
duplicate key value violates unique constraint "orders_pkey"
events في الـ broker: 1 ← event لـ order سارة اللي عمره ما اتحفظ
③ outbox: الـ order والـ event في نفس الـ transaction:
crash قبل الـ COMMIT: الـ process اتقفلت
orders: 0 و outbox: 0 ← ولا حاجة، والاتنين متفقين
بعد placeOrder: {"pending":1,"dead":0,"oldestPendingSeconds":0} والـ broker لسه فاضي: 0
relayOnce: {"published":1,"failed":0,"skipped":0} ← الـ broker: OrderPlaced#2
④ عشرين order، والـ broker واقع تلات مرات، وأربع acks ضاعوا (الـ broker استلم والـ relay ما عرفش):
الـ relay لفّ 6 مرات، والـ broker استلم 24 message لعشرين event
الـ billing: {"processed":20,"duplicate":4} ← invoices: 20 ومجموعها 210000
ومن غير inbox كان هيبقى فيه 24 invoice، يعني عميل اتحاسب مرتين
⑤ ترتيب الـ events لنفس الـ order، والأول فشل مرة:
أول لفّة: {"published":1,"failed":1,"skipped":2} ← o8 اتنشر، و o7 كله استنى
الـ broker: o8:OrderPlaced o7:OrderPlaced o7:OrderPaid o7:OrderShipped
⑥ اتنين relays شغّال
…الأول: الـ order اتحفظ، وصفر events. التاني: event لـ order سارة، والـ order نفسه اترفض. التالت: الـ crash قبل الـ COMMIT، ولا order ولا event، والاتنين متفقين. وبعد placeOrder، فيه event pending، والـ relay نشره. الرابع: الـ broker استلم أكتر من عشرين message، لأن الـ acks اللي ضاعت اتبعتت تاني. بس الـ billing عمل عشرين invoice بالضبط، والباقي duplicate. ومن غير inbox، كان فيه ناس اتحاسبت مرتين. الخامس: o7 فشل، فالتلات events بتوعه استنوا، و o8 كمّل. وفي الآخر o7 وصل بالترتيب. والسادس: الاتنين relays قسموا الشغل، ومفيش ولا message اتبعتت مرتين. والـ cleanup مسح القديم بس. ونشغّل test dot js.
المخرجات
$ node test.js ✅ withTransaction: بيرجّع الـ result، و ROLLBACK مع الـ error نفسه، والـ client بيرجع للـ pool ✅ enqueue: id كـ string، وجُوّه نفس الـ transaction، والـ payload jsonb ✅ placeOrder: الـ order والـ OrderPlaced مع بعض ✅ placeOrder: id متكرر أو total بصفر ← ولا order ولا event ✅ relayOnce: بترتيب الـ id، والـ message shape، و published_at، والمرة التانية صفر ✅ relayOnce: batchSize ✅ relayOnce: الفشل بيزوّد attempts و last_error، والمرة الجاية بتنجح ✅ relayOnce: لو message فشلت، اللي بعدها لنفس الـ aggregate بتستنى، والتانيين يكمّلوا ✅ maxAttempts: بعدها الـ message dead ومش بتتسحب، و pendingStats ✅ relayOnce: اتنين relays مع بعض، كل message مرة واحدة، والاتنين اشتغلوا 12+12 ✅ handleOnce: processed وبعدين duplicate، وكل consumer لوحده ✅ handleOnce: الـ effect فشل ← الـ inbox row والـ effect الاتنين اترجعوا، والمحاولة الجاية تشتغل ✅ cleanup و oldestPendingSeconds: المنشور القديم بس، والـ pending ما بيتمسحش ✅ الاتنين مع بعض: acks ضاعت فيه تكرار في الـ broker، بس كل order ليه invoice واحدة بالضبط 15 14/14
كله نجح. دلوقتي الـ event الواحد بيوصل بأمان. بس فيه عمليات أكبر من event واحد. order محتاج يحجز stock، ويخصم من المحفظة، ويحجز شحن. تلات services، وكل واحدة ليها database. لو الخطوة التالتة فشلت، إزاي ترجّع الاتنين اللي قبلها، من غير transaction بتجمعهم؟ الدرس الجاي: الـ sagas، بالـ orchestration والـ choreography، والـ compensations.
// orders.naive.js: الطريقة اللي كلنا بنبدأ بيها، اسمها dual write
import { pool } from "./db.js";
import { broker } from "./broker.js";
export async function placeOrder(order) {
await pool.query("INSERT INTO orders (id, customer, total_minor) VALUES ($1, $2, $3)", [order.id, order.customer, order.totalMinor]);
// ← لو الـ process وقعت هنا، أو الـ broker واقع، الـ order اتحفظ والـ event ضاع
await broker.publish({ type: "OrderPlaced", orderId: order.id, totalMinor: order.totalMinor });
}
// والـ consumer: كل message توصل، invoice جديدة
broker.subscribe("OrderPlaced", async (m) => {
await pool.query("INSERT INTO invoices (order_id, total_minor) VALUES ($1, $2)", [m.orderId, m.totalMinor]);
});-- schema.sql: الـ orders، والـ outbox جنبها في نفس الـ database، والـ inbox عند الـ consumer
DROP TABLE IF EXISTS invoices, inbox, outbox, orders;
CREATE TABLE orders (
id text PRIMARY KEY,
customer text NOT NULL,
total_minor int NOT NULL CHECK (total_minor > 0),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE outbox (
id bigserial PRIMARY KEY,
aggregate_id text NOT NULL,
type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
published_at timestamptz,
attempts int NOT NULL DEFAULT 0,
last_error text
);
-- الـ relay بيدوّر على اللي لسه ما اتنشرش بس، فالـ index ده صغير على طول
CREATE INDEX outbox_pending ON outbox (id) WHERE published_at IS NULL;
CREATE TABLE inbox (
consumer text NOT NULL,
message_id text NOT NULL,
processed_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (consumer, message_id)
);
-- مفيش unique على order_id هنا عن قصد: الـ inbox هو اللي بيمنع التكرار
CREATE TABLE invoices (
id bigserial PRIMARY KEY,
order_id text NOT NULL,
total_minor int NOT NULL
);// db.js: الـ pool، و reset بيشغّل schema.sql من الأول
import pg from "pg";
import { readFileSync } from "node:fs";
export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
export const reset = () => pool.query(readFileSync(new URL("./schema.sql", import.meta.url), "utf8"));
export const count = async (sql, params = []) => Number((await pool.query(sql, params)).rows[0].n);// lab.js: dual write بيضيّع events، وبعدين outbox و relay و inbox على Postgres حقيقي. الـ broker هنا array في الـ memory
import { pool, reset, count } from "./db.js";
import { withTransaction, enqueue, placeOrder, relayOnce, handleOnce, pendingStats, cleanup } from "./outbox.js";
const broker = {
log: [], down: 0, lostAcks: 0, delayMs: 0,
async publish(m) {
if (this.down > 0) { this.down--; throw new Error("broker unavailable"); }
if (this.delayMs) await new Promise((r) => setTimeout(r, this.delayMs));
this.log.push(m);
if (this.lostAcks > 0) { this.lostAcks--; throw new Error("ack timeout"); }
},
};
const publish = (m) => broker.publish(m);
const crash = () => { throw new Error("الـ process اتقفلت"); };
const drain = async (opts) => { let n = 0; for (let i = 0; i < 50 && (await pendingStats(pool)).pending; i++) n++, await relayOnce(pool, publish, opts); return n; };
await reset();
console.log("① dual write: احفظ وبعدين انشر، والـ process بتقع في النص:");
try { await pool.query("INSERT INTO orders VALUES ('n1', 'mahmoud', 500)"); crash(); await publish({ type: "OrderPlaced", orderId: "n1" }); }
catch (e) { console.log(" ", e.message); }
console.log(" orders:", await count("SELECT count(*) n FROM orders"), "و events في الـ broker:", broker.log.length, "← الـ billing عمره ما هيعرف");
console.log("\n② انشر الأول وبعدين احفظ، والحفظ بيفشل:");
try { await publish({ type: "OrderPlaced", orderId: "n1" }); await pool.query("INSERT INTO orders VALUES ('n1', 'sara', 700)"); }
catch (e) { console.log(" ", e.message); }
console.log(" events في الـ broker:", broker.log.length, "← event لـ order سارة اللي عمره ما اتحفظ");
await reset(); broker.log = [];
console.log("\n③ outbox: الـ order والـ event في نفس الـ transaction:");
try { await withTransaction(pool, async (c) => { await c.query("INSERT INTO orders VALUES ('o0', 'karim', 900)"); await enqueue(c, { aggregateId: "o0", type: "OrderPlaced", payload: {} }); crash(); }); }
catch (e) { console.log(" crash قبل الـ COMMIT:", e.message); }
console.log(" orders:", await count("SELECT count(*) n FROM orders"), "و outbox:", await count("SELECT count(*) n FROM outbox"), "← ولا حاجة، والاتنين متفقين");
await placeOrder(pool, { id: "o1", customer: "mahmoud", totalMinor: 44900 });
console.log(" بعد placeOrder:", JSON.stringify(await pendingStats(pool)), "والـ broker لسه فاضي:", broker.log.length);
console.log(" relayOnce:", JSON.stringify(await relayOnce(pool, publish)), "← الـ broker:", broker.log.map((m) => `${m.type}#${m.id}`).join());
await reset(); broker.log = [];
console.log("\n④ عشرين order، والـ broker واقع تلات مرات، وأربع acks ضاعوا (الـ broker استلم والـ relay ما عرفش):");
for (let i = 1; i <= 20; i++) await placeOrder(pool, { id: `o${i}`, customer: "u" + i, totalMinor: 1000 * i });
broker.down = 3; broker.lostAcks = 4;
console.log(" الـ relay لفّ", await drain({ batchSize: 5 }), "مرات، والـ broker استلم", broker.log.length, "message لعشرين event");
const results
…03 / دروس المسار (122)
- 01أهلًا بيك: يعني إيه backend، والكورس ده ماشي إزاي
- 02الملفات والفولدرات والـ terminal: تتحرك على جهازك زي المبرمجين
- 03Node وأول برنامج: تشغّل وتقرا رسالة الخطأ
- 04القيم والمتغيرات والأنواع: const و let و typeof
- 05القرارات والتكرار: if و === والـ truthy والـ loops
- 06الـ functions: parameters و return والـ arrow functions والـ scope
- 07الـ objects والـ arrays: destructuring و spread و map و filter و reduce
- 08JSON والنصوص والتواريخ: اللغة اللي الـ backend بيتكلم بيها
- 09الـ callbacks والـ closures وأول لمسة async: setTimeout
- 10الـ classes والـ errors: throw و try/catch و stack trace
- 11git: تحفَظ كل خطوة وترجعلها، وما تسرّبش أسرارك
- 12ESM و CommonJS و package exports: Node بيلاقي الـ modules إزاي
- 13الـ Promises و async/await: sequential و parallel والأخطاء والإلغاء
- 14الـ event loop والـ microtasks: Node بيعمل كل حاجة بـ thread واحد إزاي
- 15الـ Streams والـ Backpressure: ملفات أكبر من الـ memory
- 16الـ File System والـ paths: قراية وكتابة صح، ومن غير path traversal
- 17الـ Buffers والبيانات الـ binary: الـ bytes من جُوّه
- 18Crypto APIs والأسرار: random و hash و HMAC و AES-GCM
- 19الـ Worker Threads: شغل CPU تقيل من غير ما توقف الـ server
- 20الـ Errors على مستوى الـ process: operational و programmer و crash-only
- 21الـ Node test runner والـ debugging: tests من غير مكتبات
- 22HTTP من جُوّه: الـ methods والـ status codes والـ headers
- 23Express والـ Routing: الـ params والـ query والترتيب والـ Router
- 24Express 5: إيه اللي اتغيّر، وإزاي تنقل مشروع قديم
- 25الـ Middleware ودورة حياة الـ request جُوّه Express
- 26Validation للمدخلات بـ Zod: ما تصدّقش أي حاجة جاية من برة
- 27تصنيف الأخطاء و Problem Details: شكل واحد لكل الـ errors
- 28الـ CORS والـ Cookies: المتصفح بيحمي المستخدم إزاي
- 29رفع الملفات وحدود الحجم: multer من غير ثغرات
- 30الـ Realtime: Server-Sent Events و WebSockets
- 31OpenAPI والـ API Versioning: عقد مكتوب، وتغيير من غير ما تكسر حد
- 32PostgreSQL: الـ DDL وأنواع البيانات الصح لكل عمود
- 33الـ Constraints: PK و FK و UNIQUE و CHECK — الـ database بتحمي نفسها
- 34الـ Normalization والـ ERD: تقسّم البيانات صح من أول يوم
- 35الـ JOINs والتجميع: INNER و LEFT و GROUP BY و HAVING و window functions
- 36JSONB في PostgreSQL: إمتى تستخدمه، وإزاي تسأله وتعمله index
- 37الـ Indexes من جُوّه: B-tree و GIN و BRIN
- 38Partial و Covering indexes: indexes أصغر وأسرع
- 39الـ Pagination بالـ Keyset: ليه OFFSET بيبطّأ وبيكرر
- 40Full Text Search في Postgres: بحث بالكلمات، بالعربي والإنجليزي
- 41EXPLAIN ANALYZE والـ Planner: اقرا الـ plan وصلّح الـ query
- 42الـ Transactions بـ node-postgres: كله أو ولا حاجة
- 43الـ MVCC ومستويات الـ Isolation: transactions كتير في نفس الوقت
- 44الـ Locks والـ Deadlocks: مين مستني مين
- 45SKIP LOCKED و Job Leasing: Postgres كـ job queue
- 46Row Level Security والـ Multi-tenancy: كل عميل يشوف بياناته بس
- 47Migrations من غير downtime: expand و backfill و contract
- 48الـ Replication والـ Read Replicas: نسخ حية من الـ database
- 49الـ Connection Pooling و PgBouncer: connections أقل، requests أكتر
- 50الـ Partitioning والـ VACUUM والمراقبة: Postgres على المدى الطويل
- 51Redis وأنواع البيانات: strings و hashes و lists و sets و sorted sets
- 52Cache-aside والـ TTL: Redis قدام الـ database
- 53الـ Cache invalidation والـ Stampede: لما الـ key الشهير يموت
- 54Lua والـ Atomicity في Redis: كام أمر كأنهم أمر واحد
- 55الـ Rate Limiting في Redis: fixed window و sliding window و token bucket
- 56الـ Sessions في Redis: تسجيل دخول تقدر تلغيه في أي وقت
- 57Redis Streams والـ Consumer Groups: log بيتوزع على workers
- 58Pub/Sub و BullMQ: إشارات بين الـ instances، وطوابير شغل محترمة
- 59Redis لما يقع: الـ persistence (RDB و AOF) والـ replication
- 60Redis في الإنتاج: الـ ACLs والـ Cluster والمراقبة
- 61تخزين الـ Passwords بـ Argon2id: صح من أول مرة
- 62Session Fixation والـ Rotation: id جديد مع كل صلاحية جديدة
- 63الـ Cookie Flags والـ CSRF: المتصفح بيبعت الـ cookie، فلازم تتأكد مين اللي طلب
- 64الـ XSS والـ Content Security Policy: ما تخليش النص يتحوّل لـ code
- 65الـ OAuth و OpenID Connect و PKCE: login بـ Google من غير ما تشوف الـ password
- 66التأكد من الـ JWT صح: التوقيع والـ claims والـ scopes
- 67الـ BOLA و BFLA: الـ token السليم مش معناه إن الطلب مسموح
- 68الـ RBAC و ABAC والملكية: policy واحدة بدل ifs متفرقة
- 69الـ MFA والـ recovery: التاني factor اللي بجد
- 70الـ Passkeys و WebAuthn: login من غير password وضد الـ phishing
- 71الـ Threat Modeling بـ STRIDE: فكّر زي المهاجم قبل ما تكتب code
- 72الـ SQL Injection والـ Parameterization: الـ data ما تبقاش code
- 73الـ SSRF والـ Egress: ما تخليش الـ server بتاعك يزور الشبكة الداخلية
- 74الـ Security Headers والـ TLS: كل response يقول للـ browser يحمي المستخدم إزاي
- 75توقيع الـ Webhooks والـ Replay: الـ webhook ده جاي فعلًا من Stripe؟
- 76تغيير الأسرار والـ HMAC: إزاي تغيّر مفتاح من غير ما كل حاجة تقع
- 77سلسلة التوريد والـ SBOM: إنت واثق في ألف package ما تعرفهمش؟
- 78الـ Audit والـ Privacy Logs: سجّل مين عمل إيه، من غير ما تسجّل الأسرار
- 79اختبار الأمان و OWASP ASVS: الأمن جُوّه الـ CI، مش audit آخر السنة
- 80الـ Prototype Pollution والـ Resource DoS: request واحد يخلّي الكل admin أو يوقّع الـ server
- 81الـ SLI والـ SLO والـ Error Budget: إمتى تصحّي الـ on-call وإمتى تسيبه نايم
- 82الـ Event Loop Delay والـ ELU: لما Node نفسه يتخنق
- 83الـ Worker Threads والـ CPU Profiling: الشغل التقيل بره الـ event loop، والوقت راح فين
- 84الـ Memory والـ GC والـ Leaks: الـ process اللي بيكبر لحد ما يقع
- 85الـ N+1 Queries والـ DataLoader: صفحة واحدة بمِيّتين query
- 86الـ HTTP Caching والـ Compression: أسرع request هو اللي ما بيوصلش
- 87الـ Load Testing والـ Soak Testing: الـ server بيستحمل قد إيه بجد
- 88الـ Concurrency Limits والـ Bulkheads: dependency بطيئة ما توقعش الـ app كله
- 89الـ Timeouts والـ Retries والـ Circuit Breakers: إزاي تتعامل مع dependency بطيئة أو واقعة
- 90OpenTelemetry والـ Distributed Tracing: الـ request راح فين وضاع وقته فين
- 91Docker Layers والـ Build Context: ليه الـ build بياخد دقيقة وفيه الـ .env بتاعك
- 92Multi-stage Builds و Non-root: image صغيرة ومن غير root
- 93Docker Compose والـ Networks: الـ services بتلاقي بعض إزاي
- 94Docker Volumes: الـ data بتتحفَظ فين
- 95Healthcheck و Restart و Resource Limits: running مش معناها شغّال
- 96BuildKit والـ Build Secrets: الـ token ما يدخلش الـ image
- 97Reverse Proxy و TLS: اللي واقف قدام الـ app
- 98Multi-arch Images والـ Native Modules: اشتغلت عندي
- 99CI/CD و Rollback و Canary: الـ deploy يبقى ممل
- 100Kubernetes من الأول: Pods و Deployments و Services و Probes
- 101Modular Monolith و DDD: حدود واضحة من غير network
- 102Transactional Outbox و Inbox: الـ event ما يضيعش وما يتنفذش مرتينانت هنا
- 103Sagas: عملية واحدة على تلات services من غير transaction واحدة
- 104CQRS و Event Sourcing: الـ events هي الحقيقة
- 105الـ Queues والـ Brokers: BullMQ و RabbitMQ و Kafka و SQS
- 106Realtime على أكتر من instance: Redis pub/sub و presence و resume
- 107الـ Object Storage والـ Media: presigned URLs و uploads مباشرة
- 108التكامل مع الـ Email والـ Search: retries و webhooks و indexes
- 109GraphQL و gRPC في الإنتاج: contracts و DataLoader و limits و deadlines
- 110Kafka على أكتر من zone و region: الـ replication والـ ISR والـ acks والـ MirrorMaker والـ failover
- 111TypeScript و runtime boundaries: الـ types بتختفي، والـ schemas لأ
- 112ORM ولا SQL: query builders و repositories و migrations و transactions
- 113الـ Serverless من جُوّه: cold starts و freeze و timeouts و connections
- 114الـ Testing Pyramid: unit و integration و e2e على Postgres حقيقية
- 115Contract Testing و Property-Based Testing بـ fast-check
- 116الـ Payments: idempotency و webhooks و refunds و reconciliation
- 117الـ Timezones والـ Internationalization: UTC و DST والـ locales والـ plurals والفلوس
- 118الـ Privacy والـ Data Retention: consent و export و erasure و crypto shredding
- 119الـ ADRs والـ Feature Flags: القرارات المكتوبة والـ rollout من غير deploy
- 120Multi-region active-active: حل الـ conflicts بـ HLC و CRDTs
- 121Readiness و Liveness و Startup Probes: الـ pod جاهز ولا عايش؟
- 122Incident Runbooks: من الـ alert للـ postmortem