نظام تسجيل المستخدمين الموثوق: حل معضلة الكتابة المزدوجة بنمط Outbox
كيف تضمن إرسال رسائل التحقق دون فقدان للبيانات أو تعليق الحسابات عند انقطاع الاتصال بمزود البريد عبر نمط الصندوق الصادر Outbox.
✉️ Sign Up + Email Verification: إزاي تبني Registration System يعتمد عليه؟
سؤال انترفيو:
</div>المستخدم سجل حساب جديد، دخل الاسم والإيميل والباسورد وضغط Create Account... إيه اللي المفروض يحصل؟
📚 Table of Contents
- المشكلة مش في Create User
- Database First vs Email First
- Outbox Pattern
- Outbox Transaction
- Background Worker
- At-Least-Once Delivery
- Duplicate Emails
- Idempotent Activation
- Resend Activation
- Rate Limiting
- Token Security
- Token Expiration
- Eventual Consistency
- Outbox Cleanup
- CDC
- Message Queues
- Email Enumeration
- Registration Architecture
- Failure Scenarios
- Common Wrong Answers
- Interview Answer
- Production Checklist
- Official Sources
🎤 السؤال
المستخدم يدخل:
Name
Email
Password
ويضغط:
Create Account
الـflow البسيط اللي ناس كتير تتخيله:
Create User
↓
Send Activation Email
↓
Done
لكن في production، عندك عمليتين مختلفتين:
Database
+
Email Provider
والـdatabase والـemail provider مش جزء من نفس الـtransaction.
وده هو أصل المشكلة.
💥 Database First vs Email First
الحالة الأولى: Database First
نعمل:
Create User
↓
User Created ✅
↓
Send Email
↓
💥 Server Crash
النتيجة:
User exists
Email doesn't exist
المستخدم عنده account.
لكن مش قادر يفعل الحساب.
الحالة الثانية: Email First
نقول:
Send Email
↓
Email sent ✅
↓
Save User
↓
💥 Server Crash
دلوقتي:
Email exists
User doesn't exist
المستخدم يضغط activation link:
Account Not Found
أسوأ.
🧠 المشكلة الحقيقية
مش:
"أبعت الإيميل الأول ولا أحفظ المستخدم الأول؟"
المشكلة:
عندك عمليتين مستقلتين ومفيش distributed transaction بسيطة تضمن نجاحهم مع بعض.
DB Transaction
❌
Email Provider
وعشان كده محتاجين design مختلف.
📦 Outbox Pattern
الحل المشهور:
Outbox Pattern
الفكرة:
بدل ما تعمل:
DB
+
Email
في نفس اللحظة...
تعمل:
DB Transaction
├── Create User
└── Create Outbox Message
الاتنين داخل نفس transaction.
مثلاً:
BEGIN;
INSERT INTO users (...);
INSERT INTO outbox_messages (
type,
payload,
status
);
COMMIT;
لو الـtransaction نجحت:
User ✅
Outbox Message ✅
ولو فشلت:
User ❌
Outbox Message ❌
ودي أهم فكرة في الـpattern.
🔐 Outbox Transaction
مثلاً:
Create Account
│
▼
┌─────────────────────┐
│ DB Transaction │
│ │
│ Create User │
│ + │
│ Create Email Event │
│ │
└─────────┬───────────┘
│
COMMIT
│
▼
Transaction
durable
بعدها عملية إرسال الإيميل تبقى منفصلة.
وده معناه إننا فصلنا:
Business Transaction
عن:
Email Delivery
📨 شكل Outbox Message
مثلاً:
{
"id": "msg_123",
"type": "UserVerificationEmail",
"aggregateId": "user_456",
"payload": {
"userId": "user_456"
},
"status": "Pending",
"attempts": 0,
"createdAt": "2026-08-17T10:00:00Z"
}
وفي systems production ممكن تحتاج fields إضافية مثل:
availableAt
processedAt
lastError
attemptCount
lockedUntil
🤖 Background Worker
بعد ما الـrequest يعمل:
User Created
+
Outbox Created
الـAPI ترجع للمستخدم.
مثلاً:
201 Created
والـworker يبدأ:
Outbox
↓
Read Pending Message
↓
Send Email
↓
Success?
لو نجح:
status = Sent
لو فشل:
retry later
💥 ماذا لو السيرفر وقع؟
تخيل:
User Created ✅
Outbox Created ✅
Worker ❌
مفيش مشكلة.
لأن الرسالة محفوظة في database.
لما الـworker يرجع:
Read Pending
↓
Send
↓
Done
الـevent مش ضاع.
وده هو السبب الرئيسي لاستخدام Outbox Pattern.
🔁 At-Least-Once Delivery
دلوقتي تظهر مشكلة أخطر.
الـworker:
Send Email
↓
Email Provider says success
↓
💥 Worker crashes
الـworker مكنش لحق يحدث:
status = Sent
لما يقوم تاني:
Message still Pending
فيقول:
Send again
فتوصل:
Email #1
Email #2
وده ممكن يحصل حتى في systems مصممة بشكل صحيح.
ليه؟
لأن فيه window بين:
External side effect
و:
Marking message as processed
📌 At-Least-Once
عشان كده messaging systems كثيرًا ما توفر semantics أقرب إلى:
At-Least-Once Delivery
يعني النظام يحاول يضمن إن الرسالة لن تضيع بسهولة، لكن ممكن توصل أكثر من مرة.
وده معناه إن الـconsumer لازم يتعامل مع:
Duplicate Delivery
بشكل آمن.
📧 Duplicate Emails
هل email duplicate كارثة؟
مش دائمًا.
لكن UX سيئ.
عشان كده ممكن تستخدم:
Message ID
+
Idempotency / Deduplication
لكن فيه نقطة مهمة:
الـemail provider نفسه قد لا يعطيك exactly-once delivery.
لذلك لا تبني correctness الحرجة على افتراض إن email سيتسلم مرة واحدة فقط.
🔁 Idempotent Activation
تخيل المستخدم ضغط:
Verify Email
مرتين.
الطلب الأول:
Account
inactive → active
الطلب الثاني:
active → active
النتيجة النهائية نفسها.
وده مثال على:
Idempotency
يعني تنفيذ نفس العملية مرة أو عدة مرات يؤدي لنفس الـfinal state.
مثلاً:
POST /verify-email
لو token صالح وتم استخدامه:
verified = true
لو المستخدم ضغط مرة ثانية:
Already verified
بدون side effects إضافية.
🔄 Resend Activation Email
طيب المستخدم ضغط:
Resend Activation Email
عشر مرات.
لو كل مرة:
Send Email
ممكن يحصل:
10 emails
في دقيقة.
وده ممكن يتحول إلى:
Spam Abuse
+
Email Provider Cost
+
Reputation Damage
عشان كده نحتاج:
⏳ Cooldown
مثلاً:
Last sent:
10:00:00
والـpolicy:
Cooldown = 60 seconds
لو المستخدم حاول:
10:00:20
نرفض:
Try again in 40 seconds
🚦 Rate Limiting
لكن الـcooldown على user واحد مش كفاية.
المهاجم ممكن يعمل:
1000 fake accounts
وكل account يطلب:
Resend
عشان كده ممكن تعمل layers من rate limiting:
Per User
+
Per IP
+
Per Email / Destination
+
Global Provider Limit
مثلاً conceptually:
20 requests / minute / IP
لكن الأرقام ليست universal.
لازم تتحدد حسب:
Traffic
Business
Email Provider Limits
Abuse Model
🧠 Token Security
سؤال مهم:
هل نخزن activation token نفسه في database؟
مثلاً:
abc123xyz
الأفضل في الأنظمة الحساسة:
Store Hash
وتبعت:
Raw Token
للمستخدم فقط.
يعني:
Generated Token
│
├── Raw → Email
│
└── Hash → Database
ولما المستخدم يضغط:
Raw Token
↓
Hash
↓
Compare DB Hash
الفكرة إن database compromise مايديش attacker الـraw verification tokens مباشرة.
OWASP توصي في token-based account recovery بأن تكون tokens عشوائية، طويلة بما يكفي، single-use، ومحدودة الصلاحية، وأن تكون مخزنة بشكل آمن بدل التعامل معها كقيمة عادية. citeturn0search0
🎲 Token Generation
مهم جدًا:
متستخدمش:
Math.random()
أو random generator غير مناسب للأمان.
استخدم:
Cryptographically Secure Random
لأن activation token يعتبر credential مؤقت.
⏰ Token Expiration
الـtoken مينفعش يفضل صالح للأبد.
مثلاً:
Created:
10:00
Expires:
11:00
بعدها:
Token expired ❌
وده يقلل window اللي attacker يقدر يستخدم فيها token مسروق.
♻️ Single Use
بعد نجاح التفعيل:
Token
↓
Consumed
لازم ماينفعش يستخدم مرة ثانية.
ممكن تعمل:
usedAt
أو:
delete token
حسب التصميم.
🔄 Eventual Consistency
بعد استخدام Outbox:
Create Account
يحصل فورًا.
لكن:
Email
ممكن يوصل بعد:
100ms
500ms
2s
10s
حسب الـqueue والـprovider.
يعني مش كل حاجة بتحصل في نفس اللحظة.
وده مثال على:
Eventual Consistency
الحالة النهائية:
User Created
↓
Email Event Stored
↓
Worker Processes
↓
Email Sent
فالنظام ممكن يكون مؤقتًا:
User exists
Email not sent yet
وده طبيعي ومقبول في هذا النوع من الأنظمة.
🧹 Outbox Cleanup
دلوقتي تخيل:
10 million emails
وكل الرسائل القديمة تفضل في:
outbox_messages
للأبد.
الجدول هيكبر.
عشان كده ممكن تعمل:
Retention Policy
مثلاً:
Sent messages older than X days
↓
Delete / Archive
لكن خليك حذر:
ما تمسحش الرسائل اللي لسه محتاجة retry.
يعني lifecycle مثلًا:
Pending
↓
Processing
↓
Sent
↓
Retention
↓
Delete / Archive
🔭 CDC
هل لازم الـworker يعمل:
SELECT *
FROM outbox_messages
WHERE status = 'Pending'
كل ثانية؟
مش بالضرورة.
في الأنظمة الأكبر ممكن تستخدم:
Change Data Capture — CDC
الفكرة إنك تلتقط تغييرات قاعدة البيانات من transaction log / change stream بدل polling المستمر على جدول التطبيق.
مثلاً conceptually:
DB Transaction Log
↓
CDC
↓
Message Broker
↓
Email Consumer
وده ممكن يقلل polling load ويعطي event propagation أسرع.
لكن CDC مش "أفضل دائمًا".
هو إضافة architectural complexity، وقرار استخدامه يعتمد على:
Scale
Latency Requirements
Database
Operational Maturity
Existing Streaming Infrastructure
📨 Outbox + Queue
ممكن كمان يكون عندك:
API
↓
DB Transaction
├── User
└── Outbox
↓
Relay / CDC
↓
Message Queue
↓
Worker
↓
Email Provider
وهنا كل طبقة لها دور.
🐇 Message Queue
ممكن تستخدم:
- RabbitMQ
- Kafka
- Azure Service Bus
- Amazon SQS
لكن مهم جدًا:
Queue لا تحل تلقائيًا مشكلة atomicity بين Database وMessage.
لو عملت:
INSERT User
↓
Publish Queue
وحصل crash بين الاثنين:
User ✅
Message ❌
لذلك Outbox + Queue ممكن يكون design قوي جدًا:
DB Transaction
↓
Outbox
↓
Relay / CDC
↓
Queue
↓
Consumer
☁️ Azure Functions
ممكن الـconsumer يكون:
Azure Function
بدل:
Long-running Worker
مثلاً:
Queue Message
↓
Azure Function
↓
Email Provider
وده مناسب لبعض workloads الـevent-driven.
لكن Azure Functions نفسها مش replacement للـOutbox.
الـOutbox بيحل:
DB change
+
durable event
بينما Function هي مجرد طريقة لتنفيذ consumer/worker.
🕵️ Email Enumeration
دي نقطة أمنية مهمة.
تخيل endpoint:
POST /forgot-password
ويرد:
user@example.com
→ Email sent
لكن:
notfound@example.com
→ Email doesn't exist
المهاجم يقدر يجرب ملايين الإيميلات ويعرف:
Which accounts exist?
وده اسمه:
Email Enumeration
الأفضل في flows زي password recovery أو resend:
"If an account exists, we'll send you an email."
بدل:
This email is registered.
مع الحفاظ قدر الإمكان على response timing متقارب وعدم تسريب الاختلاف من خلال status/response behavior.
OWASP توصي بأن تكون رسائل الاستجابة متسقة حتى لا يتمكن المهاجم من تحديد ما إذا كان الحساب موجودًا. citeturn0search0
🏗️ Registration Architecture
Architecture محترمة ممكن تكون:
┌───────────────┐
│ Browser │
└───────┬───────┘
│
▼
┌───────────────┐
│ Register │
│ API │
└───────┬───────┘
│
▼
┌────────────────────┐
│ DB Transaction │
│ │
│ Create User │
│ + │
│ Create Outbox │
└─────────┬──────────┘
│
COMMIT
│
▼
┌───────────────┐
│ Outbox Relay │
└───────┬───────┘
│
▼
┌───────────────┐
│ Message Queue │
└───────┬───────┘
│
▼
┌───────────────┐
│ Email Worker │
└───────┬───────┘
│
▼
┌───────────────┐
│ Email Provider│
└───────────────┘
🔄 Failure Scenarios
1. User Created + Server crashes
User ✅
Outbox ✅
Worker ❌
بعد restart:
Outbox
↓
Worker
↓
Email
2. Email sent + Worker crashes
Email Provider ✅
Worker state update ❌
بعد restart:
Pending
↓
Retry
↓
Duplicate email possible
وده طبيعي مع at-least-once processing.
3. Database transaction fails
User ❌
Outbox ❌
مفيش email يتبعت.
4. Email Provider down
User ✅
Outbox ✅
Worker retries
الـaccount موجود، والرسالة مستنية.
5. User clicks activation twice
First click:
inactive → active
Second click:
already active
No harmful side effect.
6. Attacker spams resend
Cooldown
+
Per-IP Rate Limit
+
Per-account Rate Limit
+
Provider limits
❌ Common Wrong Answers
❌ "أحفظ المستخدم وبعدين SendEmail"
مش كفاية.
لأن:
DB success
+
Email failure
ممكن يسيب النظام في state ناقصة.
❌ "أبعت الإيميل الأول"
برضه مش كفاية.
ممكن:
Email success
+
DB failure
❌ "أعمل Database Transaction حوالين SendEmail"
مش كفاية.
الـemail provider مش جزء من نفس database transaction.
❌ "Queue تحل المشكلة كلها"
لا.
لو:
DB commit
+
Queue publish
مش atomic:
DB succeeds
Queue publish fails
لسه عندك مشكلة.
عشان كده Outbox مفيد.
❌ "Exactly Once"
لا تفترضها.
في distributed systems، الـconsumer غالبًا لازم يكون قادرًا على التعامل مع duplicate delivery.
❌ "لو الإيميل اتبعت خلاص أعمل Sent"
مش دائمًا.
لو حصل crash قبل تسجيل الحالة، ممكن الرسالة تتبعت مرة ثانية.
وده سبب أهمية idempotent consumers وduplicate tolerance.
❌ "أخزن activation token plain text"
أفضل تخزين hash للـtoken عندما يكون ذلك مناسبًا.
❌ "Resend مفيهوش مشكلة"
ممكن يتحول إلى:
Spam
+
Cost
+
Abuse
🎯 Interview Answer
لو الـinterviewer قال:
"المستخدم عمل Sign Up وعايز تبعتله verification email. هتصممها إزاي؟"
ممكن تجاوب:
"مش هحاول أخلي إنشاء الـuser وإرسال الإيميل في نفس transaction لأن الـdatabase والـemail provider نظامين مختلفين. هعمل DB transaction واحدة فيها إنشاء المستخدم وإنشاء Outbox Message تحتوي على event لإرسال verification email. لو transaction نجحت، الـuser والـmessage يبقوا durable مع بعض. بعد كده worker أو relay يقرأ الـoutbox ويبعت الإيميل، ومع الفشل يعمل bounded retries."
"لازم أفترض إن delivery ممكن تكون at-least-once، وبالتالي الـemail والـactivation flow لازم يتحملوا duplicate processing. الـactivation token هيكون cryptographically random، limited lifetime، single-use، ومخزن بشكل آمن، ويفضل تخزين hash بدل الـraw token. رابط التفعيل لازم يكون idempotent."
"كمان هحط cooldown وrate limiting على resend، وخصوصًا per-account/per-IP، وأمنع email enumeration برسائل responses متسقة. وعلى مستوى التشغيل هعمل retention/cleanup للـoutbox، monitoring، retry visibility، ولو الـscale كبير ممكن أستخدم CDC أو relay إلى message broker."
🧠 Mental Model
احفظها كده:
SIGN UP
│
▼
┌──────────────┐
│ DB TX │
│ │
│ User │
│ + │
│ Outbox Event │
└──────┬───────┘
│
COMMIT
│
▼
Outbox Relay
│
▼
Queue
│
▼
Worker
│
▼
Email Provider
│
┌─────┴─────┐
▼ ▼
Success Failure
│ │
▼ ▼
Sent Retry
والـsecurity:
Verification Token
│
├── Cryptographically Random
├── Short Expiration
├── Single Use
└── Hash in DB
والـabuse protection:
Resend
│
├── Cooldown
├── Per-user limit
├── Per-IP limit
└── Provider/global limit
📊 أهم الـConcepts
| Concept | المشكلة التي يحلها |
|---|---|
| Outbox Pattern | منع ضياع event بين DB وexternal messaging |
| Background Worker | فصل العمل البطيء عن request |
| Retry | التعامل مع transient failures |
| At-Least-Once | تقليل احتمال ضياع الرسائل |
| Idempotency | منع duplicate side effects |
| Cooldown | منع resend spam |
| Rate Limiting | منع abuse على نطاق أكبر |
| Token Hashing | تقليل أثر DB compromise |
| Token Expiration | تقليل lifetime للـcredential |
| CDC | نشر تغييرات DB بدون polling مستمر |
| Queue | فصل producers عن consumers |
| Eventual Consistency | قبول تأخر أجزاء النظام عن بعضها |
| Enumeration Protection | منع كشف الحسابات الموجودة |
🏆 Production Checklist
Registration
- Validate input
- Normalize email
- Enforce unique email constraint
- Hash password with a password-hashing algorithm
- Create user + outbox event in same DB transaction
- Do not depend on email success for DB commit
Verification Token
- Cryptographically secure random token
- Sufficient entropy
- Short expiration
- Single use
- Store hash where appropriate
- Invalidate after successful verification
Delivery
- Durable outbox
- Background worker / relay
- Bounded retries
- Backoff
- Failure tracking
- Duplicate tolerance
- Monitoring
Abuse Protection
- Resend cooldown
- Per-account rate limiting
- Per-IP rate limiting
- Provider/global limits
- Enumeration-resistant responses
Operations
- Outbox retention
- Cleanup/archive policy
- Dead-letter/failure handling where appropriate
- Metrics
- Alerts
- Traceability
- Reconciliation
📚 Official Sources
OWASP — Forgot Password Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
مفيد جدًا لفهم token security، expiration، single-use، enumeration protection، والـpassword recovery flows.
OWASP — Authentication Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
مرجع رسمي لمبادئ authentication وحماية الحسابات.
Microsoft — Transactional Outbox Pattern
https://learn.microsoft.com/en-us/azure/architecture/databases/guide/transactional-outbox-cosmos
شرح رسمي لفكرة transactional outbox وربط database transaction بالرسائل بشكل موثوق.
Microsoft — Asynchronous Request-Reply Pattern
https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply
مفيد لفهم فصل request عن الأعمال التي تستغرق وقتًا أطول وتنفيذها بشكل asynchronous.
Debezium — Change Data Capture
https://debezium.io/documentation/reference/stable/
مرجع رسمي لتقنية CDC والتقاط تغييرات قواعد البيانات.
RabbitMQ Documentation
توثيق رسمي للـmessage broker وأنماط الـmessaging.
Apache Kafka Documentation
https://kafka.apache.org/documentation/
توثيق رسمي لـKafka والـevent streaming.
Azure Service Bus
https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview
توثيق Microsoft الرسمي لخدمة messaging المدارة.
<div align="center">
🚀 الخلاصة
إرسال Email التفعيل شكله:
SendEmail()
لكن النظام الحقيقي أقرب إلى:
Create User
+
Create Outbox
↓
Durable Transaction
↓
Relay / CDC
↓
Queue
↓
Worker
↓
Email Provider
↓
Retry / Monitoring
ومعاه:
Secure Token
+
Expiration
+
Single Use
+
Idempotency
+
Rate Limiting
+
Enumeration Protection
الفرق بين مبرمج بيكتب SendEmail() ومهندس بيبني Registration System موثوق هو إنه بيفكر في اللي يحصل لما كل حاجة تمشي غلط. 🔥
منشورات مقترحة
مشاريع ذات صلة
منصة تعليمية متكاملة تتيح إجراء الاختبارات المحاكاة المؤقتة، واستعادة جلسة الامتحان من التخزين المحلي، وتقييم قدرات الطلاب بتقارير تحليلية.