DDIA·مطالعهSTUDY

نقشه راهRoadmap / فصل 5Chapter 5 / حل تمرین‌هاSolutions

حل آموزشی تمرین‌های فصل 5Chapter 5 exercise solutions, explained

هر حل از سؤال مصاحبه شروع می‌کند، فرض‌های آموزشی را روشن می‌گوید، گام‌به‌گام استدلال می‌کند و یک نمودار دارد که خواندنش را یاد می‌گیری. هر حل به تمرین خودش برمی‌گردد. Each solution starts from the interview question, states its teaching assumptions, reasons step by step, and shows one diagram you learn to read aloud. Every solution links back to its exercise.

33 حل تشریحیworked solutions· متناظر با بخش تمرین‌های فصل 5Companion to Chapter 5 exercises

11 تمرین ۱ — بایت‌ها را دستی بشمارExercise 1 — count the bytes by hand

L1 · پایهL1 · Foundationsوابسته به نقشRole-specific25 دقیقه بحث25 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
با یک رکورد کوچک تفاوت JSON، MessagePack و Protobuf را در schema، حجم و debugging توضیح بده. یک tag را دستی decode کن. فیلد email اضافه کن و دو مسیر binary round-trip و تبدیل به JSON را از نظر حفظ unknown field مقایسه کن.
Compare JSON, MessagePack and Protobuf, decode one tag, then add email. Test unknown-field preservation in binary round-trips against JSON conversion or field-by-field copying.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

‏JSON نام همه فیلدها را حمل می‌کند (۸۱ بایت آموزشی، انسان‌خوان، بدون قاعده تکامل)؛ MessagePack همان مدل را باینری می‌کند (۶۶ بایت آموزشی) ولی نام‌ها همچنان در داده‌اند؛ Protobuf نام‌ها را با tag جایگزین می‌کند (۳۳ بایت آموزشی) به‌علاوه قواعد تکامل مشخص. نکته مصاحبه عدد بایت‌ها نیست بلکه آزمون email است: ‏round-trip باینری با runtime درست، تگ ناشناخته ۴ را حفظ می‌کند، ولی تبدیل از مسیر JSON یا کپی فیلدبه‌فیلد بی‌صدا حذفش می‌کند. سازگاری سیمی با حفظ معنا یکی نیست و هیچ‌کدام با سازگاری کد برابر نیستند.
JSON carries every field name (81 teaching bytes, human-readable, no evolution rules); MessagePack binarizes the same model (66 teaching bytes) but still ships the names; Protobuf replaces names with tags (33 teaching bytes) plus declared evolution rules. The interview point is not the byte totals but the email test: a binary round-trip through a proper runtime preserves unknown tag 4, while converting through JSON or copying field-by-field silently drops it. Wire compatibility is not meaning preservation, and neither equals code compatibility.

صورت مسئله و فرض‌هاProblem and assumptions

  • فقط رکورد آموزشی: userName=Martin, favoriteNumber=1337, interests=[daydreaming, hacking]. مجموع بایت‌های ۸۱/۶۶/۳۳ اعداد آموزشی فصل‌اند، نه بنچمارک.
  • مسیر باینری با کد تولیدشده Protobuf است که runtime آن فیلدهای ناشناخته را نگه می‌دارد؛ مسیر JSON به آبجکت JSON (یا پیام تازه ساخته‌شده فیلدبه‌فیلد) decode می‌شود که جایی برای بایت‌های ناشناخته ندارد.
  • فیلد تازه email با تگ تازه ۴ است؛ تگ بازنشسته دوباره استفاده نمی‌شود.
  • Teaching record only: userName=Martin, favoriteNumber=1337, interests=[daydreaming, hacking]. Byte totals 81/66/33 are teaching numbers from the chapter, not benchmarks.
  • Binary path uses generated Protobuf code whose runtime retains unknown fields; the JSON path decodes into a JSON object (or a fresh message built field-by-field), which has nowhere to store unknown bytes.
  • The new field is email with fresh tag 4, never reusing a retired tag.

چرا این روش و این عددها؟Why this method and these numbers?

اندازهٔ wire را با جمع tag، length و payload می‌سنجیم چون هر سه واقعاً ارسال می‌شوند. varint در هر بایت هفت بیت داده دارد، پس برای عدد مثبت ۱۳۳۷ دو بایت لازم است چون از ۱۲۷ بزرگ‌تر و از ۱۶۳۸۳ کوچک‌تر است. تفاوت ۸۱/۶۶/۳۳ به متن، فاصله‌ها، encoding و schema همین نمونه وابسته است. حفظ unknown field به runtime و مسیر round-trip وابسته است؛ JSON جای بایت ناشناخته را تضمین نمی‌کند.

Sum tag, length and payload because all travel on the wire. A varint carries seven data bits per byte: positive 1337 needs two bytes because it exceeds 127 but is below 16384. The 81/66/33 totals depend on this sample’s text, whitespace, encoding and schema. Unknown-field preservation depends on runtime and round-trip path; JSON does not guarantee retention of unknown bytes.

مدل ذهنیMental model

بایت‌ها را نامه و schema را لغت‌نامه خواننده فرض کن. JSON هر بار واژه‌های کامل را پست می‌کند؛ Protobuf کدهای شماره‌دار کوتاه می‌فرستد و دو طرف از قبل روی دفترچه کد توافق کرده‌اند. وقتی واژه تازه (تگ ۴) می‌رسد که خواننده قدیمی نیاموخته، فقط یک سؤال مهم است: خواننده قدیمی پاکت دربسته را دست‌نخورده نگه می‌دارد، یا بازش می‌کند، واژه‌هایی که می‌شناسد را رونویسی می‌کند و بقیه را دور می‌ریزد؟
Think of the bytes as a letter and the schema as the reader's dictionary. JSON posts the full words every time; Protobuf posts short numbered codes and both sides agree on the codebook in advance. When a new word (tag 4) arrives that the old reader never learned, the only question that matters is: does the old reader pocket the sealed envelope untouched, or open it, copy out the words it knows, and throw the rest away?

حل گام‌به‌گامStep by step

  1. نام‌ها روی سیم JSON نام‌ها را می‌نویسد: userName و favoriteNumber و interests داخل همان ۸۱ بایت آموزشی سفر می‌کنند، پس هر خواننده‌ای — و هر انسانی با ویرایشگر متن — ساختار را می‌بیند. هزینه: حجم، ابهام اعداد، و صفر قاعده تکامل مگر خودت registry و قرارداد اضافه کنی.
  2. همان مدل، باینری MessagePack همان مدل بدون schema را باینری می‌کند: بایت‌های نوع و طول جای گیومه و دونقطه می‌نشینند و به حدود ۶۶ بایت آموزشی می‌رسیم. نام‌ها همچنان سفر می‌کنند، اشکال‌زدایی decoder می‌خواهد، و تکامل دقیقاً به‌اندازه قراردادهای JSON خودت منضبط است — یا بی‌انضباط.
  3. یک تگ را دستی decode کن Protobuf نام‌ها را حذف می‌کند و به‌جایش تگ + نوع سیمی می‌نویسد: بایت 0x0a یعنی فیلد ۱ (0x0a >> 3) با نوع سیمی ۲ (0x0a & 7 یعنی طول‌دار)، و ۱۳۳۷ با varint دوبایتی b9 0a می‌رود. جمع: ۳۳ بایت آموزشی. تغییر نام فیلد آزاد است؛ تغییر تگ ممنوع.
  4. دو مسیر، دو سرنوشت email را با تگ ۴ اضافه کن و دو مسیر را اجرا کن. round-trip باینری (کد قدیمی تولیدشده می‌خواند و بازمی‌نویسد): runtime بایت‌های تگ ۴ را دست‌نخورده عبور می‌دهد و خواننده جدید نشانی را سالم می‌یابد. تبدیل JSON یا کپی فیلدبه‌فیلد: گام میانی فقط فیلدهای ۱ تا ۳ را می‌شناسد، پس تگ ۴ جایی برای زندگی ندارد و خواننده جدید email گمشده می‌بیند. سیم یکسان، نتیجه برعکس — تفاوت در قرارداد decoder است نه بایت‌ها.
  1. Names on the wire JSON writes the names: userName, favoriteNumber, interests travel inside the 81 teaching bytes, so any reader — and any human with a text editor — sees the structure. Cost: size, number ambiguity, and zero evolution rules unless you add a registry and conventions yourself.
  2. Same model, binary MessagePack binarizes the same schemaless model: type and length bytes replace quotes and colons, landing near 66 teaching bytes. Names still travel, debugging needs a decoder, and evolution is exactly as disciplined — or undisciplined — as your JSON conventions.
  3. Decode one tag by hand Protobuf drops the names and writes tag + wire type instead: byte 0x0a is field 1 (0x0a >> 3) with wire type 2 (0x0a & 7, length-prefixed), and 1337 rides as the two-byte varint b9 0a. Total: 33 teaching bytes. Renaming a field is free; changing a tag is forbidden.
  4. Two paths, two fates Add email as tag 4 and run the two paths. Binary round-trip (old generated code reads, rewrites): the runtime carries tag 4's bytes through untouched, and the new reader finds the address intact. JSON conversion or field-by-field copy: the middle step only knows fields 1–3, so tag 4 has nowhere to live and the new reader sees a missing email. Same wire, opposite outcomes — the difference is the decoder contract, not the bytes.

مثال محاسبه‌شدهWorked example

رکورد آموزشی {"userName":"Martin","favoriteNumber":1337,"interests":["daydreaming","hacking"]}: ‏JSON بدون فاصله = ۸۱ بایت، ‏MessagePack حدود ۶۶ بایت، ‏Protobuf برابر ۳۳ بایت (۸ بایت فیلد ۱ + ۳ بایت فیلد ۲ + ۲۲ بایت دو آیتم تکراری). افزودن email (تگ ۴، ۱۳ بایت متن + ۲ بایت قاب) رکورد باینری را به ۴۸ بایت می‌رساند؛ خواننده باینری قدیمی همچنان فیلدهای ۱ تا ۳ را می‌فهمد و تگ ۴ را ندیده عبور می‌دهد.

Teaching record {"userName":"Martin","favoriteNumber":1337,"interests":["daydreaming","hacking"]}: spaceless JSON = 81 bytes, MessagePack ≈ 66 bytes, Protobuf = 33 bytes (8 for field 1 + 3 for field 2 + 22 for the two repeated items). Adding email (tag 4, 13 text bytes + 2 framing bytes) grows the binary record to 48 bytes; the old binary reader still parses fields 1–3 and forwards tag 4 unseen.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · round-trip باینری تگ ۴ را نگه می‌داردBinary round-trip keeps tag 4 · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
نسخه جدید (v2.2) رکورد را با تگ‌های ۱ تا ۴ می‌نویسد در حالی که نسخه قدیمی (v2.1) هنوز سرو می‌دهد. شش فلش را بالا‌به‌پایین دنبال کن.
Version 2.2 writes the record with tags 1–4 while 2.1 still serves. Follow the six arrows top to bottom.
  1. دو فلش اول: نوشتن رکورد تازه و خواندن آن توسط کد قدیمی — تگ ۴ ناشناخته به دست قدیمی می‌رسد.
  2. فلش پررنگ میانی (بازنویسی) قهرمان است: runtime بایت‌های ناشناخته را دست‌نخورده عبور می‌دهد.
  3. دو فلش آخر: خواندن دوباره توسط جدید و پاسخ «tag 4 intact» — ایمیل سالم است.
  1. First two arrows: the fresh write, then the old read — unknown tag 4 lands in old hands.
  2. The bold middle arrow (write-back) is the hero: the runtime forwards unknown bytes untouched.
  3. Last two arrows: the new re-read with the reply 'tag 4 intact' — the email survives.

نقطهٔ تصمیم: نقطه تصمیم فلش بازنویسی است: عبور بایت ناشناخته یا بازسازی از دانسته‌ها؟

رد خرابی: اگر runtime ناشناخته‌ها را می‌انداخت، خواندن دوباره ایمیل گمشده می‌دید — همان شکل مسیر دوم.

تضمین نشان‌داده‌شده: شکل ترتیب و حفظ را نشان می‌دهد؛ ثابت نمی‌کند هر runtimeای نگه می‌دارد — مال خودت را تست کن.

تضمین نشان‌نداده‌شده: درباره معنای تگ ۴ چیزی نمی‌گوید: حفظ بایت با فهم درست یکی نیست.

Decision point: The decision point is the write-back arrow: forward unknown bytes or rebuild from known fields?

Failure trace: If the runtime dropped unknowns, the re-read would see a missing email — exactly the second path's shape.

Guarantee shown: The figure shows order and preservation; it does not prove every runtime preserves — test your own.

Not shown: It says nothing about tag 4's meaning: kept bytes are not understood bytes.

خودآزماییSelf-check

چرا کد قدیمی بایت‌هایی را عبور می‌دهد که نمی‌فهمد؟

پاسخ: چون سازگاری forward یعنی حمل، نه فهم: ناشناخته را نادیده بگیر ولی بایت‌هایش را نگه دار.

Why does old code forward bytes it cannot parse?

Answer: Because forward compatibility means carrying, not comprehending: ignore the unknown but keep its bytes.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · کپی JSON تگ ۴ را می‌اندازدJSON copy drops tag 4 · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
همان نوشتن تازه، ولی این بار یک پراکسی قدیمی وسط است که رکورد را از فیلدهای شناخته‌شده بازمی‌سازد.
The same fresh write, but now an old proxy in the middle rebuilds the record from known fields.
  1. فلش اول: بایت‌های ۱ تا ۴ به پراکسی می‌رسند — هنوز کامل است.
  2. فلش دوم: پراکسی فقط ۱ تا ۳ را کپی می‌کند؛ تگ ۴ جایی برای زندگی ندارد.
  3. فلش چین‌چین آخر: هشدار پایین‌دستی «email missing» — گم‌شدن بی‌صدا دیر کشف می‌شود.
  1. First arrow: bytes 1–4 reach the proxy — still whole.
  2. Second arrow: the proxy copies only 1–3; tag 4 has nowhere to live.
  3. Last dashed arrow: the downstream 'email missing' alert — silent loss is found late.

نقطهٔ تصمیم: نقطه تصمیم فلش کپی است: مدل میانی فقط دانسته‌ها را دارد.

رد خرابی: هیچ‌جا خطایی بالا نمی‌آید؛ مانیتور سبز روی داده گمشده — بدترین نوع خرابی.

تضمین نشان‌داده‌شده: شکل نقطه مرگ تگ ۴ را نشان می‌دهد؛ راه‌حل (عبور بایت یا ارتقای پراکسی) بیرون شکل است.

تضمین نشان‌نداده‌شده: درباره این‌که کدام کتابخانه‌ها نگه می‌دارند چیزی نمی‌گوید — قرارداد هر runtime را جدا بخوان.

Decision point: The decision point is the copy arrow: the middle model holds only known fields.

Failure trace: No error is raised anywhere; a green monitor over lost data — the worst kind of failure.

Guarantee shown: The figure shows where tag 4 dies; the remedy (byte forwarding or proxy upgrade) is outside it.

Not shown: It says nothing about which libraries preserve — read each runtime's contract separately.

خودآزماییSelf-check

با یک تغییر کوچک چه چیزی تگ ۴ را نجات می‌دهد؟

پاسخ: عبور بایت‌های ناشناخته دست‌نخورده به‌جای کپی فیلدبه‌فیلد — یا به‌روزرسانی schema پراکسی.

What one small change saves tag 4?

Answer: Forwarding unknown bytes untouched instead of field-by-field copying — or upgrading the proxy schema.

نمودار اسمبل نهاییFinal assembly diagram · دو سرنوشت تگ ناشناخته ۴: round-trip باینری نگهش می‌دارد، کپی JSON حذفش می‌کندTwo fates for unknown tag 4: binary round-trip keeps it, JSON copy drops it · دو مسیرtwo pathsباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
نسخه جدید اپ رکوردهای کاربر را با فیلد email می‌نویسد در حالی که سرورهای قدیمی هنوز زنده‌اند. رکورد را در هر مسیر چپ‌به‌راست دنبال کن و ببین خواننده جدید آخر چه می‌بیند.
A new app version writes user records with an email field while old servers are still live. Follow the record left to right through each path and watch what the final new reader sees.
  1. اول نوار بالا: نویسنده جدید تگ‌های ۱ تا ۴ را می‌فرستد — رکورد کامل، هنوز چیزی گم نشده.
  2. خواننده قدیمی فقط ۱ تا ۳ را می‌شناسد ولی runtime آن بایت‌های تگ ۴ را جیب می‌زند؛ جعبه بازنویسی («bytes untouched») قهرمان مسیر A است.
  3. نوار پایین: پراکسی قدیمی فقط دانسته‌هایش را کپی می‌کند («copies 1-3»)، پس رکورد تازه بدون تگ ۴ متولد می‌شود.
  4. جعبه‌های آخر: KEPT سبز در برابر LOST قرمز — پیام پانوشت این است که سازگاری سیمی همان حفظ معنا نیست.
  1. Top lane first: new writer emits tags 1–4 — the full record, nothing lost yet.
  2. Old reader knows only 1–3 but its runtime pockets tag 4's bytes; the write-back box ('bytes untouched') is the hero of Path A.
  3. Bottom lane: the old proxy copies only what it knows ('copies 1–3'), so the fresh record is born missing tag 4.
  4. End boxes: green KEPT versus red LOST — the footer moral is that wire compatibility is not meaning preservation.

نقطهٔ تصمیم: نقطه تصمیم جعبه میانی هر مسیر است: کد قدیمی بایت‌های ناشناخته را عبور می‌دهد یا از فیلدهای شناخته‌شده بازمی‌سازد؟

رد خرابی: مسیر B را جعبه‌به‌جعبه دنبال کن: تگ ۴ دقیقاً در زیرنویس «copies 1-3» پراکسی قدیمی می‌میرد — رکورد تازه بدون آن متولد می‌شود، پس «email gone» خواننده جدید دو جعبه زودتر رقم خورده. هیچ‌جا خطایی بالا نمی‌آید و به همین دلیل گم‌شدن بی‌صداست.

تضمین نشان‌داده‌شده: شکل تضمین می‌کند مسیر از قالب مهم‌تر است: بایت‌های ورودی یکسان، نتیجه برعکس. تضمین نمی‌کند فلان کتابخانه خاص فیلد ناشناخته را نگه می‌دارد — این را باید برای هر runtime جدا آزمایش کرد.

تضمین نشان‌نداده‌شده: درباره سازگاری معنایی چیزی نمی‌گوید: تگ ۴ حفظشده اگر شماره تگی روزی دوباره استفاده شود باز هم می‌تواند اشتباه فهمیده شود.

Decision point: The decision point is the middle box of each path: does the old code forward opaque bytes or rebuild from known fields?

Failure trace: Trace Path B box by box: the old proxy's 'copies 1-3' subtitle is where tag 4 dies — the fresh record is born without it, so the new reader's 'email gone' is decided two boxes earlier. No error is raised anywhere, which is why the loss is silent.

Guarantee shown: The figure guarantees the path matters more than the format: same input bytes, opposite outcomes. It does not guarantee any particular library keeps unknown fields — that must be tested per runtime.

Not shown: It says nothing about semantic compatibility: a preserved tag 4 can still be misinterpreted if a tag number is ever reused.

خودآزماییSelf-check

کدام مسیرهای شکل تگ ۴ را حفظ می‌کنند — و چرا مسیر JSON نمی‌تواند؟

پاسخ: فقط round-trip باینری با runtime نگه‌دارنده. متن JSON جایی برای بایت‌های تگ ۴ ندارد، پس هر چیزی که از مدل JSON-شکل بگذرد آن را می‌بازد.

Which paths in the diagram preserve tag 4 — and why can't the JSON path?

Answer: Only the binary round-trip through a preserving runtime. JSON text has no slot for tag 4's bytes, so anything that passes through a JSON-shaped model loses it.

گزینه‌های جایگزینAlternatives

  • ماندن روی JSON با registry و بازبینی سخت‌گیرانه: اشکال‌زدایی انسان‌خوان و تبادل بین‌سازمانی می‌ماند، به قیمت payload بزرگ‌تر و قواعد تکامل دستی.
  • Stay on JSON with a registry and strict review: keeps human-readable debugging and cross-org exchange, at the cost of bigger payloads and hand-enforced evolution rules.

سناریوهای خرابیFailure scenarios

  • پراکسی قدیمی رکورد را فیلدبه‌فیلد بازنویسی می‌کند: تگ ۴ در همان گام کپی حذف می‌شود و هر خواننده جدید پایین‌دستی برای همیشه email گمشده می‌بیند — گم‌شدن داده، نه خطای parse.
  • استفاده دوباره از شماره تگ ۴ برای فیلد دیگر: ایمیل‌های رکوردهای قدیمی بی‌صدا مقدار فیلد تازه می‌شوند — خرابی‌ای که هیچ تست سازگاری بعداً نمی‌گیرد.
  • Old proxy rewrites the record field-by-field: tag 4 is dropped at the copy step and every downstream new reader permanently sees a missing email — data loss, not a parse error.
  • Reusing tag 4's number for a different field later: old records' emails silently become the new field's values — corruption no compatibility test can catch after the fact.

کد قابل اجراRunnable code

اجراشده و تأییدشده در همین محیطExecuted and verified in this environment

این کد آموزشی و ساده‌سازی‌شده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچ‌کس این‌شکلی کد پروداکشن نمی‌زند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.

import java.nio.charset.StandardCharsets;

public class TagDemo {
    static byte[] varint(long n) {
        byte[] tmp = new byte[10];
        int i = 0;
        while (true) {
            int b = (int) (n & 0x7F);
            n >>>= 7;
            if (n != 0) {
                tmp[i++] = (byte) (b | 0x80);
            } else {
                tmp[i++] = (byte) b;
                break;
            }
        }
        byte[] out = new byte[i];
        System.arraycopy(tmp, 0, out, 0, i);
        return out;
    }

    static String hex(byte[] a) {
        StringBuilder sb = new StringBuilder();
        for (byte b : a) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    static byte[] join(byte[]... parts) {
        int n = 0;
        for (byte[] p : parts) {
            n += p.length;
        }
        byte[] out = new byte[n];
        int o = 0;
        for (byte[] p : parts) {
            System.arraycopy(p, 0, out, o, p.length);
            o += p.length;
        }
        return out;
    }

    public static void main(String[] args) {
        byte[] v = varint(1337);
        System.out.println("1337 as varint: " + hex(v));
        byte[] rec = join(
                new byte[]{0x0a, 0x06}, "Martin".getBytes(StandardCharsets.UTF_8),
                new byte[]{0x10}, v,
                new byte[]{0x1a, 0x0b}, "daydreaming".getBytes(StandardCharsets.UTF_8),
                new byte[]{0x1a, 0x07}, "hacking".getBytes(StandardCharsets.UTF_8));
        System.out.println("record bytes: " + rec.length);
        int first = rec[0] & 0xFF;
        System.out.printf("first tag byte 0x%02x: field %d, wire type %d (2 = length-prefixed)%n",
                first, first >> 3, first & 7);
    }
}

اجرا: javac TagDemo.java && java TagDemo — فقط استاندارد جاوا (JDK 27)، فوری. به‌جای ۱۳۳۷ عدد خودت را بگذار و ببین طول varint در مرزهای ۶۴ و ۸۱۹۲ چطور عوض می‌شود.

خروجی مورد انتظار: 1337 به‌صورت varint برابر b90a؛ بایت‌های رکورد: ۳۳؛ بایت اول تگ 0x0a: فیلد ۱، نوع سیمی ۲.

Run: javac TagDemo.java && java TagDemo — Java stdlib only (JDK 27), instant. Change 1337 to your own number and watch the varint length move at the 64 and 8192 boundaries.

Expected output: 1337 as varint: b90a; record bytes: 33; first tag byte 0x0a: field 1, wire type 2 (2 = length-prefixed).

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

راستی‌آزمایی کن: بعد از افزودن تگ ۴ دوباره کد کن و مطمئن شو فیلدهای ۱ تا ۳ یکسان decode می‌شوند؛ بایت‌ها را به کد تولیدشده واقعی قدیمی بده و مطمئن شو تگ ۴ در بازنویسی باینری می‌ماند ولی در round-trip از مسیر JSON می‌رود؛ مطمئن شو هیچ تگ بازنشسته‌ای دوباره استفاده نشده.
Verify: re-encode after adding tag 4 and confirm fields 1–3 decode identically; feed the bytes to real generated old code and confirm tag 4 survives a binary rewrite but vanishes through a JSON round-trip; assert no retired tag number is reused.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): proxy قدیمی پیام را field-by-field به پیام جدید کپی می‌کند؛ چه داده‌ای گم می‌شود؟Follow-up (Senior): An old proxy copies recognized fields into a fresh message. What data disappears?

بایت‌های تگ ۴ اصلاً به پیام تازه نمی‌رسند: پراکسی به مدلی decode می‌کند که فقط فیلدهای ۱ تا ۳ را دارد، بعد از همان مدل دوباره encode می‌کند. email برای هر خواننده پایین‌دستی رفته، با این‌که هیچ خطایی رخ نداده. راه‌حل: به‌جای کپی فیلدهای شناخته‌شده، بایت‌های ناشناخته را دست‌نخورده عبور بده (یا schema پراکسی را به‌روز کن).

The bytes for tag 4 never make it into the fresh message: the proxy decodes into a model that only has fields 1–3, then re-encodes from that model. The email is gone for every downstream reader, even though nothing errored. The fix is to forward opaque bytes (or upgrade the proxy's schema) instead of copying recognized fields.

اشتباهات رایجCommon mistakes

  • حفظ کردن ۸۱/۶۶/۳۳ به‌جای مکانیزم. اندازه‌ها با نام و مقدار فیلدها عوض می‌شوند؛ مصاحبه قرارداد decoder را نمره می‌دهد.
  • گفتن «Protobuf همیشه فیلد ناشناخته را نگه می‌دارد». فقط runtime درست در مسیر باینری این کار را می‌کند — تبدیل JSON و کپی‌های دستی حذفشان می‌کنند.
  • یکی دانستن سازگاری سیمی با سازگاری کد. کد قدیمی می‌تواند بایت‌هایی را عبور دهد که نمی‌فهمد — اصل سازگاری forward همین است.
  • Memorizing 81/66/33 instead of the mechanism. Sizes change with field names and values; the decoder contract is what the interview grades.
  • Saying 'Protobuf always preserves unknown fields'. Only a proper runtime on the binary path does — JSON conversion and hand-rolled copies drop them.
  • Treating wire compatibility as code compatibility. Old code can forward bytes it cannot interpret — that is the whole point of forward compatibility.

منابع:Sources: Protobuf · proto3 language guide · Atlassian · engineering interview handbook

22 تمرین ۲ — تکاملِ schema زیرِ فشارExercise 2 — schema evolution under pressure

L2 · SeniorL2 · Seniorاولویت اصلیCore priority40 دقیقه بحث40 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
برای Avro و Protobuf افزودن فیلد، rename و تغییر cardinality را بررسی کن. جهت writer→reader، default، alias، tag و binary/JSON را دقیق مشخص کن. scalar-to-array را با فیلد جدید و دورهٔ dual compatibility منتشر کن؛ compatibility test و rollback داشته باش.
Compare Avro and Protobuf field addition, rename and cardinality changes. Specify writer/reader direction, aliases, defaults and tags. Migrate cardinality with a new field and compatibility tests; distinguish parseability from lossless semantics.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

افزودن فیلد اختیاری در هر دو سمت امن است: تگ تازه Protobuf (کد قدیمی رد می‌کند و نگه می‌دارد، کد جدید به داده قدیمی مقدار پیش‌فرض می‌دهد) و فیلد Avro با default خواننده (اضافه نادیده گرفته می‌شود، غایب پر می‌شود). تغییر نام در Protobuf آزاد است (نام روی سیم نیست) ولی در Avro فقط backward است (alias خواننده جدید داده قدیمی را می‌خواند؛ خواننده‌های قدیمی نام تازه را هیچ‌وقت نمی‌آموزند). تبدیل یک مقدار به چند مقدار درجا ناامن است — به نوع فیلد، packed بودن و جهت وابسته است — پس با فیلد تازه، دوره سازگاری دوگانه، تست سازگاری registry و نقشه rollback مهاجرت کن. حذف فقط وقتی آخرین مصرف‌کننده قدیمی رفت.
Adding an optional field is safe on both sides: a fresh Protobuf tag (old code skips and preserves, new code defaults old data) and an Avro field with a reader default (extras ignored, missing filled). Renaming is free in Protobuf (names are not on the wire) but backward-only in Avro (the new reader's alias reads old data; old readers never learn the new name). Changing one value into many in place is unsafe — it depends on field type, packed-ness and direction — so migrate via a NEW field with a dual-compat period, a registry compatibility test, and a rollback plan. Removal only after the last old consumer is gone.

صورت مسئله و فرض‌هاProblem and assumptions

  • رکورد آموزشی پروفایل کاربر با نام، سن و یک راه تماس (تلفن/email)؛ هر جهت پایین نویسنده و خواننده‌اش را صریح می‌گوید.
  • مگر خلافش گفته شود مسیرها باینری‌اند؛ رندر JSONای Protobuf (تکراری‌ها به‌صورت آرایه) قواعد متفاوت و پراتلاف‌تری دارد و هرجا که گاز می‌گیرد گفته می‌شود.
  • بررسی‌های سازگاری registry و پنجره‌های انتشار پایین اعداد سیاستی آموزشی‌اند، نه ردپای اندازه‌گیری‌شده.
  • Teaching user-profile record with a name, an age and one phone/email contact; every direction below names its writer and reader explicitly.
  • Binary paths unless noted; JSON rendering of Protobuf (repeated as arrays) follows different, lossier rules and is called out where it bites.
  • Registry compatibility checks and rollout windows below are teaching policy numbers, not measured traces.

چرا این روش و این عددها؟Why this method and these numbers?

سازگاری را جهت‌دار می‌سنجیم چون reader تازه و reader قدیم اطلاعات یکسانی ندارند؛ alias تازه نمی‌تواند reader قدیم را تغییر دهد. در Protobuf tag هویت wire است، برای همین rename باینری با تغییر tag یکسان نیست. تبدیل چند مقدار به یک مقدار ممکن است parse شود ولی بقیه را دور بریزد؛ پس معیار اصلی حفظ معناست. فیلد تازه مسیر rollback می‌دهد چون مصرف‌کنندهٔ قدیم هنوز فیلد قبلی را می‌خواند؛ حذف آن فقط بعد از خروج آخرین reader قدیمی مجاز است.

Compatibility is directional because new and old readers know different schemas; a new alias cannot teach an old reader. Protobuf wire identity is the tag, so binary renaming differs from changing tags. Reading many values as one may parse yet discard values, making semantics the criterion. A new field preserves rollback while old readers use the old field; remove it only after the last old reader retires.

مدل ذهنیMental model

قبل از «سازگار است؟» همیشه بپرس «کی نوشت، کی می‌خواند؟». ‏backward یعنی خواننده جدید نویسنده قدیمی را می‌فهمد؛ ‏forward یعنی خواننده قدیمی از نویسنده جدید جان سالم به در می‌برد. و parse شدن بدون خطا با حفظ معنا یکی نیست: خواننده رشته تکراری می‌تواند بایت‌های packed قدیمی را هم به‌اندازه مقدارها، خوشحال به آشغال تبدیل کند.
Always ask 'who wrote this, who reads it?' before 'is it compatible?'. Backward means the NEW reader understands the OLD writer; forward means the OLD reader survives the NEW writer. And parsing without an error is not the same as keeping the meaning: a repeated string reader can 'read' old packed bytes into garbage just as happily as into values.

حل گام‌به‌گامStep by step

  1. افزودن: امن در هر دو سمت افزودن اختیاری در هر دو قالب: ‏Protobuf تگ تازه می‌دهد — خواننده‌های قدیمی بایت‌هایش را رد می‌کنند و نگه می‌دارند (forward)، خواننده‌های جدید روی داده قدیمی پیش‌فرض می‌گذارند (backward). ‏Avro فیلدی با default خواننده اضافه می‌کند — خواننده‌های جدید رکوردهای قدیمی را پر می‌کنند، قدیمی‌ها اضافه را نادیده می‌گیرند. تگ‌های بازنشسته را در فهرست reserved بگذار و هرگز دوباره استفاده نکن.
  2. تغییرنام: آزاد در برابر فقط-backward تغییرنام، نامتقارن: تغییرنام در Protobuf هیچ هزینه‌ای ندارد چون نام‌ها هرگز به سیم نمی‌رسند — همان تگ، هر دو جهت سالم. ‏Avro با نام جفت می‌کند، پس خواننده جدید به alias نام قدیمی نیاز دارد: جدید-قدیمی‌خوان کار می‌کند (backward)، ولی خواننده‌های قدیمی روبه‌روی داده تازه‌نوشته نام تازه را جفت نمی‌کنند (forward می‌شکند). هرگز فرض نکن alias هر دو جهت کار می‌کند.
  3. تعداد: هرگز بازنوع نکن تغییر تعداد درجا، ناامن: تبدیل تکی به repeated در Protobuf به نوع فیلد، packed بودن فرم سیمی و جهتی که از مرز نسخه‌ها می‌گذرد وابسته است — ارتقای عددی unpackشده ممکن است لنگ‌لنگان کار کند ولی رشته‌ها و فرم‌های packed گم می‌کنند یا به‌هم می‌ریزند؛ Avro هیچ قاعده resolutionای ندارد که یک مقدار را به چند مقدار تبدیل کند. parse شدن بی‌خطا این‌جا هیچ‌چیز را درباره مقدارها ثابت نمی‌کند.
  4. مهاجرت امن با فیلد تازه مهاجرت کن: emails را کنار email اضافه کن، در پنجره سازگاری هر دو را بنویس، خواننده‌ها را گروه‌به‌گروه به فیلد تازه ببر، بعد قدیمی را deprecated کن و — فقط وقتی آخرین مصرف‌کننده قدیمی رفت — حذفش کن. اول خواننده‌ها، بعد نویسنده‌ها، حذف آخر.
  5. تست و rollback هر گام را پیش از deploy با بررسی سازگاری registry دروازه‌بانی کن و rollbackای داشته باش که نسخه قبلی schema به‌علاوه باینری قبلی را برمی‌گرداند؛ در طول پنجره خطاهای خواندن هر نسخه و شمارنده‌های حذف فیلد ناشناخته را بپا.
  1. Add: safe on both sides Add optional, both formats: Protobuf assigns a fresh tag — old readers skip and preserve its bytes (forward), new readers default it on old data (backward). Avro adds a field with a reader default — new readers fill old records, old readers ignore the extra. Remove retired tags to a reserved list and never reuse them.
  2. Rename: free vs backward-only Rename, asymmetric: Protobuf renames cost nothing because names never reach the wire — same tag, both directions fine. Avro matches by name, so the new reader needs an alias of the old name: new-reads-old works (backward), but old readers facing new-written data never pair the fresh name (forward fails). Never assume the alias works both ways.
  3. Cardinality: never retype Cardinality in place, unsafe: Protobuf singular-to-repeated depends on the field type, whether the wire form is packed, and which direction crosses versions — numeric unpacked upgrades can limp along while strings and packed forms lose or garble; Avro has no resolution rule that turns one value into many. Parsing without error here proves nothing about the values.
  4. The safe migration Migrate with a NEW field: add emails alongside email, dual-write both during the compat window, move readers to the new field one cohort at a time, then deprecate and — only after the last old consumer is gone — remove the old field. Readers-first, writers-second, drop-last.
  5. Test and rollback Gate every step with a registry compatibility check before deploy and a rollback that restores the previous schema version plus the previous binary; monitor per-version read errors and unknown-field-drop counters during the window.

طراحی مشخصConcrete design

پروتو: message Profile { string name = 1; int32 age = 2; string email = 3; } — تغییرنام name آزاد، افزودن repeated string emails = 4 (تگ تازه)، تگ‌های بازنشسته reserve. Avro: رکورد با name و age دارای default و فیلد union تازه emails با پیش‌فرض خالی؛ تغییرنام فقط با aliases: ["oldName"] روی خواننده جدید. انتشار: بررسی registry ← deploy خواننده‌های متحمل ← نوشتن دوگانه قدیم+جدید ← کوچ گروه‌های خواننده ← deprecated ← حذف پس از مهلت مصرف‌کننده‌های عقب‌مانده.

Proto: message Profile { string name = 1; int32 age = 2; string email = 3; } — rename name freely, add repeated string emails = 4 (fresh tag), reserve retired tags. Avro: record with name, age with defaults, and the new union field emails defaulting empty; rename via aliases: ["oldName"] on the new reader only. Rollout: registry check → deploy tolerant readers → dual-write old+new → migrate reader cohorts → deprecate → remove after the lagging-consumer deadline.

مثال محاسبه‌شدهWorked example

تغییر (نویسنده به خواننده)ProtobufAvro
افزودن اختیاریB هست، F هستB هست، F هست (با default)
تغییرنامB هست، F هستB هست (با alias)، F نیست
تکی به چندتایی درجاوابسته به نوع و جهت: ممنوعبدون قاعده: ممنوع
تکی به چندتایی با فیلد تازهپس از نوشتن دوگانه هر دو هستپس از نوشتن دوگانه هر دو هست
Change (writer to reader)ProtobufAvro
Add optionalB ok, F okB ok, F ok (needs default)
RenameB ok, F okB ok (alias), F no
One to many, in placetype/packing/direction-dependent: do notno rule: do not
One to many, new fieldB ok, F ok after dual-writeB ok, F ok after dual-write
گام 1 از 2 · خط فکریStep 1 of 2 · reasoning thread · ریل مهاجرت با فیلد تازه: اول خواننده، آخر حذفNew-field migration rail: readers first, drop last · گردش‌کارworkflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
یک تماس تکی باید چندتایی شود در حالی که مصرف‌کننده‌های قدیمی ماه‌ها عقب‌اند. ریل را چپ‌به‌راست دنبال کن.
One contact must become many while old consumers lag months behind. Follow the rail left to right.
  1. دو گره اول: پیش‌نویس تغییر و دروازه بررسی registry — ناسازگار برگردد، سازگار جلو برود.
  2. سه گره میانی: خواننده‌های متحمل، نوشتن دوگانه، کوچ گروه‌به‌گروه — ترتیب امن همین است.
  3. دو خروجی آخر: بازنشستگی فقط پس از رفتن آخرین قدیمی؛ جهش خطا به rollback می‌رسد.
  1. First two nodes: draft the change, then the registry gate — incompatible loops back, compatible moves on.
  2. Middle three nodes: tolerant readers, dual-write, cohort-by-cohort move — this order is the safe one.
  3. Final two exits: retirement only after the last old reader is gone; an error spike reaches rollback.

نقطهٔ تصمیم: نقطه تصمیم دروازه registry است: بدون حکم سازگار، هیچ deployای.

رد خرابی: حذف زودهنگام فیلد قدیمی یا تغییر درجا — هر دو در این ریل جایی ندارند و به سطر قرمز ماتریس می‌رسند.

تضمین نشان‌داده‌شده: شکل ترتیب انتشار را نشان می‌دهد؛ مهلت حذف را تله‌متری مصرف‌کننده تعیین می‌کند، نه شکل.

تضمین نشان‌نداده‌شده: حکم هر تغییر خاص (نوع، packing، جهت) را نشان نمی‌دهد — آن کار بررسی registry است.

Decision point: The decision point is the registry gate: no compatible verdict, no deploy.

Failure trace: Early removal of the old field or an in-place retype — neither has a place on this rail and both land on the matrix's red row.

Guarantee shown: The figure shows deploy order; consumer telemetry sets the removal deadline, not the figure.

Not shown: It does not verdict any specific change (type, packing, direction) — that is the registry check's job.

خودآزماییSelf-check

چرا rollback هم schema و هم باینری را برمی‌گرداند؟

پاسخ: چون خواننده و نویسنده جفت‌اند: schema تازه با باینری قدیمی (یا برعکس) همان ناسازگاری را برمی‌گرداند.

Why does rollback restore both the schema and the binary?

Answer: Because reader and writer are a pair: a new schema with an old binary (or vice versa) brings the same incompatibility back.

نمودار اسمبل نهاییFinal assembly diagram · Avro در برابر Protobuf روی افزودن، تغییرنام و cardinality — با جهتAvro vs Protobuf on add, rename and cardinality — with direction · جدول ماتریسmatrix tableباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
اسکیمای پروفایل کاربر باید فیلد اختیاری بگیرد، از تغییرنام جان سالم به در ببرد و یک راه تماس را به چندتا تبدیل کند — در حالی که کلاینت‌های موبایل قدیمی ماه‌ها عقب‌اند. ماتریس را سطر‌به‌سطر بخوان.
A user-profile schema must gain an optional field, survive a rename, and turn one contact into many — while old mobile clients lag months behind. Read the matrix row by row.
  1. اول سربرگ: B یعنی خواننده جدید با نویسنده قدیمی، F یعنی خواننده قدیمی با نویسنده جدید — هر خانه پایین را با همین عینک بخوان.
  2. سطر سبز: افزودن در هر دو سمت امن است، با تگ تازه در چپ و default خواننده در راست.
  3. سطر دوشقه: تغییرنام در سمت تگ آزاد است ولی در سمت نام با alias و فقط backward.
  4. سطر قرمز به‌علاوه پانوشت: تعداد از مسیر فیلد تازه و نوشتن دوگانه می‌گذرد — تنها parse شدن هرگز ایمنی را ثابت نمی‌کند.
  1. Header first: B means the new reader with the old writer, F the old reader with the new writer — every cell below is read in that light.
  2. Green row: adding is safe on both sides, with a fresh tag on the left and a reader default on the right.
  3. Split row: rename is free on the tag side but alias-backed and backward-only on the name side.
  4. Red row plus footer: cardinality goes through a new field and dual-write — parsing alone never proves safety.

نقطهٔ تصمیم: نقطه تصمیم سطر سوم است: هر تغییر تعداد باید از ستون «فیلد تازه + نوشتن دوگانه» بگذرد — هرگز از «درجا».

رد خرابی: سطر قرمز را دنبال کن: تغییر تعداد درجا در هر دو سمت parse می‌شود ولی مقدارها را خراب می‌کند — ماتریس دقیقاً همان‌جا «ممنوع» نشان می‌دهد و پیام پانوشت («قابل‌تجزیه بودن همان بی‌اتلاف بودن نیست») نام تله را می‌گوید.

تضمین نشان‌داده‌شده: شکل حکم‌های صریح با جهت را برای سه تغییر تضمین می‌کند. همه ترکیب‌های نوع و packing را برنمی‌شمارد — آن کار بررسی registry است.

تضمین نشان‌نداده‌شده: درباره رندرهای JSON چیزی نمی‌گوید، جایی که فیلدهای تکراری و پیش‌فرض‌ها قواعدی متفاوت از مسیرهای باینری شکل دارند.

Decision point: The decision point is the third row: any cardinality change must leave the 'new field + dual-write' column — never the 'in place' one.

Failure trace: Trace the red row: an in-place cardinality change parses on both sides yet corrupts values — the matrix shows 'do not' exactly there, and the footer moral ('parseable is not lossless') names the trap.

Guarantee shown: The figure guarantees direction-explicit verdicts for the three changes. It does not enumerate every type-and-packing combination — that is what the registry check is for.

Not shown: It says nothing about JSON renderings, where repeated fields and defaults follow different rules than the binary paths shown.

خودآزماییSelf-check

کدام سطر بین دو ستون نامتقارن است و علتش چیست؟

پاسخ: سطر تغییرنام: ‏Protobuf هر دو جهت سبز می‌ماند ولی Avro فقط-backward نشان می‌دهد. نامتقارنی از این می‌آید که نام‌ها روی سیم Avro زندگی می‌کنند (جفت‌شدن با نام) ولی روی سیم Protobuf نه (جفت‌شدن با تگ).

Which row is asymmetric between the two columns, and what causes it?

Answer: The rename row: Protobuf stays green both ways while Avro shows backward-only. The asymmetry comes from names living on the Avro wire (matched by name) but not on the Protobuf wire (matched by tag).

گزینه‌های جایگزینAlternatives

  • بازنویسی یک‌جای داده ذخیره‌شده به schema تازه: بعدش برای همیشه یک نسخه، به قیمت downtime مهاجرت و کپی‌های آرشیوی که هنوز شکل قدیمی را دارند.
  • Stop-the-world rewrite of stored data to the new schema: one version forever after, at the cost of migration downtime and archived copies that still carry the old shape.

سناریوهای خرابیFailure scenarios

  • تغییر تکی‌به‌چندتایی درجا روی رشته‌ها از مسیر رندر JSON: رشته‌های تکی قدیمی و آرایه‌های تازه هر دو جهت parse می‌شوند ولی مقدارها خراب‌اند — بدترین خرابی چون مانیتورها سبز می‌مانند.
  • حذف فیلد قدیمی وقتی مصرف‌کننده‌های عقب‌مانده هنوز می‌خوانندش: decodeهایشان پیش‌فرض می‌گذارد یا داده موردنیاز را می‌اندازد — حذف را با شاهد عقب‌ماندگی مصرف‌کننده زمان‌بندی کن، نه با تقویم.
  • In-place singular-to-repeated on strings via JSON rendering: old single strings and new arrays parse in both directions yet values corrupt — the worst failure because monitors stay green.
  • Removing the old field while lagging consumers still read it: their decodes default or drop data they still need — schedule removal by consumer-lag evidence, not by calendar.

کد قابل اجراRunnable code

اجراشده و تأییدشده در همین محیطExecuted and verified in this environment

این کد آموزشی و ساده‌سازی‌شده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچ‌کس این‌شکلی کد پروداکشن نمی‌زند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.

public class CompatDemo {
    // Teaching model of the chapter's evolution rules (not the real
    // libraries): prints the verdict matrix and measures packed vs
    // unpacked repeated-int32 bytes to show why packing matters.

    static int varintLen(int n) {
        int l = 1;
        while ((n >>>= 7) != 0) {
            l++;
        }
        return l;
    }

    public static void main(String[] args) {
        int[] vals = {25, 31, 44};
        int unpacked = 0;
        int packedBody = 0;
        for (int v : vals) {
            unpacked += 1 + varintLen(v);
            packedBody += varintLen(v);
        }
        int packed = 1 + varintLen(packedBody) + packedBody;
        System.out.println("repeated int32 x3 unpacked bytes: " + unpacked);
        System.out.println("repeated int32 x3 packed bytes: " + packed);
        String[][] rows = {
            {"add optional", "B ok F ok", "B ok F ok (default)"},
            {"rename", "B ok F ok", "B ok (alias) F no"},
            {"one-to-many in place", "depends: do not", "no rule: do not"},
        };
        for (String[] r : rows) {
            System.out.println(r[0] + " | protobuf " + r[1] + " | avro " + r[2]);
        }
        if (unpacked != 6 || packed != 5) {
            throw new AssertionError("packing math moved");
        }
        System.out.println("packing check: unpacked=6 packed=5, verdicts as taught");
    }
}

اجرا: javac CompatDemo.java && java CompatDemo — فقط استاندارد جاوا (JDK 27)، فوری. این برنامه مدل آموزشی قواعد فصل است، نه خود کتابخانه‌ها؛ بررسی واقعی با Schema Registry محلی آزمایشگاه انجام می‌شود.

خروجی مورد انتظار: تکراری x3: unpacked برابر ۶ بایت و packed برابر ۵ بایت؛ سپس سه سطر حکم (افزودن، تغییرنام، تعداد درجا) و تأیید packing check.

Run: javac CompatDemo.java && java CompatDemo — Java stdlib only (JDK 27), instant. This program is a teaching model of the chapter's rules, not the libraries themselves; the real check runs against a lab Schema Registry.

Expected output: repeated int32 x3 unpacked bytes: 6; repeated int32 x3 packed bytes: 5; add optional | protobuf B ok F ok | avro B ok F ok (default); rename | protobuf B ok F ok | avro B ok (alias) F no; one-to-many in place | protobuf depends: do not | avro no rule: do not; packing check: unpacked=6 packed=5, verdicts as taught.

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

به‌ازای هر نسخه schema اندازه بگیر: نرخ خطای decode، شمارنده حذف فیلد ناشناخته، و سهم رکوردهایی که هنوز فیلد قدیمی را دارند؛ اگر نرخ خطای گروهی در پنجره تکان خورد هشدار بده. تمرین rollback: نسخه قبلی schema و باینری را برگردان و مطمئن شو رکوردهای قدیم و جدید هنوز خوانده می‌شوند.
Measure per schema version: decode-error rate, unknown-field-drop counter, and the share of records still carrying the old field; alert if any cohort's error rate moves during the window. Rollback drill: restore the previous schema version and binary, confirm old and new records still read.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): consumerهای قدیمی ماه‌ها عقب‌اند؛ چه زمانی tag یا فیلد قدیمی را حذف می‌کنی؟Follow-up (Senior): Old consumers are months behind. When can you remove the old field or tag?

فقط وقتی تله‌متری ثابت کند هیچ خواننده‌ای نیازش ندارد: صفر خواندن تگ/فیلد قدیمی در یک پنجره نگهداشت کامل (پنجره آموزشی: ماه‌ها عقب‌ماندگی مصرف‌کننده)، registry آن را deprecated کرده، و آرشیوها یا مهاجرت کرده‌اند یا به خواننده‌های کد قدیمی قرنطینه شده‌اند. تا آن روز تگ reserved می‌ماند و خواننده متحمل.

Only when telemetry proves no reader needs it: zero reads of the old tag/field across a full retention window (teaching window: months of consumer lag), the registry marks it deprecated, and archives are either migrated or quarantined to old-code readers. Until then the tag stays reserved and the reader stays tolerant.

اشتباهات رایجCommon mistakes

  • فرض این‌که alias در Avro هر دو جهت کار می‌کند. فقط به خواننده جدید نام قدیمی را می‌آموزد؛ خواننده‌های قدیمی از نام تازه بی‌خبر می‌مانند.
  • گفتن تکی‌به‌چندتایی «سازگار است چون parse می‌شود». قابل‌تجزیه بودن بدون نگاشت بی‌اتلاف مقدار، مانیتور سبز روی داده خراب است.
  • افزودن فیلد Avro بدون default و مقصر دانستن registry. قاعده صریح است: بدون default، سازگاری backward نیست.
  • Assuming an Avro alias works both ways. It only teaches the NEW reader the old name; old readers stay ignorant of the new one.
  • Calling singular-to-repeated 'compatible because it parses'. Parseability without a lossless value mapping is a green monitor over corrupt data.
  • Adding an Avro field without a default and blaming the registry. The rule is explicit: no default, no backward compatibility.

منابع:Sources: Protobuf · proto3 language guide · Avro 1.12 · schema resolution

33 تمرین ۳ — از تئوری تا عملیاتExercise 3 — from theory to operations

L3 · Staff / PrincipalL3 · Staff / Principalاولویت اصلیCore priority45 دقیقه بحث45 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
workflow موجودی، پرداخت و ارسال را در برابر crash و retry طراحی کن. شناسهٔ هر گام، log پایدار، نتیجهٔ ذخیره‌شده و compensation را مشخص کن. تغییر ترتیب گام‌ها، branching و schema را برای invocationهای در جریان نسخه‌بندی کن و هزینهٔ recovery را بگو.
Design inventory/payment/shipping workflow recovery with stable step IDs, durable results and compensation. Version changes to step order, branching and payloads for running invocations.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

به هر گام شناسه پایدار بده (order id + step name + code version)، نتیجه هر گام را پیش از اعلام موفقیت در لاگ پایدار بنویس، و بازپخش را با همان شناسه‌ها دوباره قدم بزن: گام‌های ثبت‌شده رد می‌شوند و نتیجه قبلی‌شان برمی‌گردد. گام‌های برگشت‌ناپذیر جبران روبه‌جلو (saga) می‌گیرند، نه بازگشت در زمان. تغییر ترتیب، شاخه یا payload را با نسخه تازه کد منتشر کن — invocationهای در جریان با همان قدیمی تمام می‌شوند. هزینه recovery خواندن‌های محدود بازپخش است به‌علاوه، برای ارسال‌کننده بدون کلید، یک جاروی مغایرت‌گیری.
Give every step a stable id (order id + step name + code version), append each result to the durable log BEFORE reporting success, and replay by re-walking with the same ids: logged steps are skipped with old results returned. Irreversible steps get forward compensations (saga), never rollbacks in time. Ship step-order, branch or payload changes as a NEW code version — in-flight invocations finish on the old one. Recovery cost is bounded replay reads plus, for the keyless shipper, a reconciliation sweep.

صورت مسئله و فرض‌هاProblem and assumptions

  • گردش‌کار آموزشی سه‌گامی سفارش: رزرو موجودی، شارژ کارت، ارسال مرسوله. کرش هرجا ممکن است، از جمله میان اثر و نوشتن لاگ — بدترین حالت.
  • درگاه پرداخت کلید idempotency می‌پذیرد؛ ارسال‌کننده طرح پایه روی شناسه گام ما تکراری‌زدایی می‌کند. ارسال‌کننده بدون کلید سناریوی پیگیری است.
  • اعداد هزینه recovery پایین کران‌های آموزشی همین گردش‌کارند، نه ردپای اندازه‌گیری‌شده.
  • Teaching three-step order workflow: reserve stock, charge card, ship parcel. Crash may strike anywhere, including between the effect and the log append — the worst case.
  • Payment gateway accepts an idempotency key; the base-design shipper dedups on our step id. The keyless shipper is the follow-up scenario.
  • Recovery-cost numbers below are teaching bounds for this workflow, not measured traces.

چرا این روش و این عددها؟Why this method and these numbers?

نتیجهٔ step باید ثبت شود تا replay همان تصمیم را ادامه دهد؛ اما crash پس از charge و پیش از ثبت نتیجه هنوز مبهم است. کلید ثابتِ همان step به gateway اجازه می‌دهد retry را همان عملیات بداند؛ شناسهٔ تازه در هر retry این خاصیت را از بین می‌برد. نسخهٔ workflow در invocation ثبت می‌شود تا replay بعد از deploy کد جدید مسیر دیگری نرود. تعداد step و هزینهٔ recovery، ویژگی سناریو هستند و باید با تعداد invocation و مدت retention مقیاس داده شوند.

Persist each step result so replay follows the original decision. A crash after charging but before logging is still ambiguous; a stable step key lets the gateway recognize the retry, while a new key per retry defeats it. Bind workflow version to invocation so deployments do not change replay paths. Recovery cost scales with invocation count, step count and retention, not a universal constant.

مدل ذهنیMental model

موتور پس از کرش فراموشی کامل دارد جز یک چیز: لاگ پایدار. بازپخش یعنی قدم‌زدن دوباره همان مسیر کد با همان شناسه‌ها و پرسیدن از لاگ در هر گام: قبلاً انجام شده؟ پس استفاده مجدد. جبران سفر در زمان نیست — گام تازه روبه‌جلوست که اثر قدیمی را خنثی می‌کند (بازپرداخت پس از شارژ).
The engine has total amnesia after a crash except for one thing: the durable log. Replay is re-walking the same code path with the same step ids and asking the log at every step: done already? Then reuse. Compensation is not time travel — it is a new forward step that undoes an old effect (refund after charge).

حل گام‌به‌گامStep by step

  1. شناسه پایدار شناسه هر گام از سه تکه ساخته می‌شود: شناسه سفارش + نام گام + نسخه کد (order:1001:ship:v1). در retry همان شناسه تکرار می‌شود، پس اثر دوباره با همان کلید به مقصد می‌رسد و تکراری‌زدایی ممکن می‌شود.
  2. اول لاگ، بعد اعلام نتیجه هر گام موفق پیش از اعلام موفقیت در لاگ پایدار نوشته می‌شود. اگر کرش میان اثر و لاگ بیفتد، گام «باز» می‌ماند و دوباره اجرا می‌شود — برای همین اثرها باید idempotent باشند.
  3. بازپخش و تحویل تکراری restart تاریخچه را می‌خواند و با همان شناسه‌ها قدم می‌زند: reserve و charge رد می‌شوند (نتیجه قدیمی برمی‌گردد) و فقط ship باز اجرا می‌شود. تحویل تکراری نتیجه ship هم فقط reuse است، نه مرسوله دوم.
  4. جبران روبه‌جلو اگر ship پس از charge ناممکن شود، تاریخ به عقب برنمی‌گردد: گام تازه refund شارژ را برمی‌گرداند و release موجودی را آزاد می‌کند. هر جبران خودش شناسه پایدار و نتیجه ثبت‌شده دارد.
  5. نسخه‌بندی و هزینه تغییر ترتیب گام‌ها، شاخه‌بندی یا payload با نسخه تازه (v2) منتشر می‌شود؛ invocationهای در جریان با v1 تمام می‌شوند چون تاریخچه v1 فقط با کد v1 قدم زده می‌شود. هزینه recovery: بازخوانی تاریخچه (کران آموزشی: تعداد گام‌ها) به‌علاوه جاروی مغایرت‌گیری برای ارسال‌کننده بدون کلید.
  1. Stable ids Each step id has three parts: order id + step name + code version (order:1001:ship:v1). Retries repeat the same id, so a repeated effect reaches the destination under the same key and dedups.
  2. Log first, then report Each successful step's result lands in the durable log before success is reported. A crash between effect and log leaves the step 'open' to run again — which is why effects must be idempotent.
  3. Replay and duplicates The restart loads history and re-walks with the same ids: reserve and charge are skipped (old results returned) and only ship re-runs. Duplicate delivery of the ship result is also just reuse, never a second parcel.
  4. Forward compensation If shipping becomes impossible after charging, history does not rewind: a fresh refund step returns the charge and a release step frees the stock. Each compensation carries its own stable id and logged result.
  5. Versioning and cost Step-order, branch or payload changes ship as a new version (v2); in-flight invocations finish on v1 because v1 history is only ever walked by v1 code. Recovery cost: re-reading history (teaching bound: number of steps) plus a reconciliation sweep for the keyless shipper.

طراحی مشخصConcrete design

اجزا: orchestrator (زمان‌بندی، مالک دروازه نسخه)، لاگ پایدار (write-ahead، تاریخچه هر invocation)، اجراکننده‌های گام (رزرو/شارژ/ارسال، هرکدام کلیددار)، جبران‌کننده‌ها (بازپرداخت، آزادسازی، لغو مرسوله). مسیر نوشتن: اجرای گام ← نوشتن نتیجه ← اعلام. مسیر خواندن (بازپخش): بارگذاری تاریخچه ← قدم‌زدن دوباره کد با همان شناسه‌ها ← ثبت‌شده رد، باز اجرا. کلیدها: order:{id}:reserve:{v} و charge و ship.

Components: orchestrator (schedules, owns the version gate), durable log (write-ahead, per-invocation history), step executors (reserve/charge/ship, each keyed), compensation handlers (refund, release, cancel-parcel). Write path: execute step → append result → report. Read path (replay): load history → re-walk code with same ids → skip logged, run open. Keys: order:{id}:reserve:{v}, order:{id}:charge:{v}, order:{id}:ship:{v}.

مثال محاسبه‌شدهWorked example

اجرای آموزشی order-1001 (خروجی واقعی برنامه): reserve و charge اجرا و ثبت می‌شوند؛ کرش پیش از ship می‌زند؛ restart هر دو را با skip, reuse بازپخش می‌کند؛ ship یک بار اجرا و ثبت می‌شود؛ تحویل تکراری ship همان parcel-9 را استفاده می‌کند. حالت پایانی: ۳ کلید ثبت‌شده، ۱ مرسوله، ۱ شارژ — هزینه بازپخش ۲ خواندن تاریخچه، ۰ اثر تکراری.

Teaching run order-1001 (real program output): reserve and charge execute and log; the crash hits before ship; the restart replays both as skip, reuse; ship executes once and logs; a duplicate ship delivery reuses parcel-9. End state: 3 logged keys, 1 parcel, 1 charge — replay cost 2 history reads, 0 repeated effects.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · کرش پیش از ship: فقط گام باز اجرا می‌شودCrash before ship: only the open step re-runs · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
سفارش ۱۰۰۱ شارژ شده ولی نتیجه ship ثبت نشده و worker مرده. دو نوار را بالا‌به‌پایین بخوان.
Order 1001 is charged but the ship result was never logged and the worker died. Read the two bands top to bottom.
  1. نوار اول: شارژ، ثبت نتیجه C، بعد ship — و کرش پیش از ثبت نتیجه S.
  2. نوار دوم با retry شروع می‌شود؛ موتور از لاگ می‌پرسد: C انجام‌شده، S باز.
  3. فقط ship با همان شناسه دوباره می‌رود و «parcel-9 (deduped)» برمی‌گردد؛ پایان «order complete».
  1. First band: charge, log result C, then ship — and the crash lands before result S is logged.
  2. Band two opens with the retry; the engine asks the log: C done, S open.
  3. Only ship goes again under the same id and 'parcel-9 (deduped)' returns; the end reads 'order complete'.

نقطهٔ تصمیم: نقطه تصمیم پرسش از لاگ است: کدام گام باز است؟ فقط همان اجرا می‌شود.

رد خرابی: اگر نتیجه C ثبت نشده بود، شارژ هم دوباره می‌رفت — و کلید idempotency درگاه جلوی برداشت دوم را می‌گرفت.

تضمین نشان‌داده‌شده: شکل قرارداد بازپخش را نشان می‌دهد: رد ثبت‌شده‌ها، اجرای بازها.

تضمین نشان‌نداده‌شده: اثر دقیقاً یک‌بار را نشان نمی‌دهد — آن به کلید مقصد بیرونی وابسته است.

Decision point: The decision point is the log lookup: which step is open? Only that one runs.

Failure trace: Had result C never been logged, charging would re-run too — and the gateway idempotency key would block the second debit.

Guarantee shown: The figure shows the replay contract: skip the logged, run the open.

Not shown: It does not show exactly-once effects — those depend on the outside destination's key.

خودآزماییSelf-check

چرا موتور پس از کرش شارژ را دوباره اجرا نمی‌کند؟

پاسخ: چون نتیجه C در لاگ پایدار است؛ بازپخش ثبت‌شده را رد می‌کند و نتیجه قبلی را برمی‌گرداند.

Why doesn't the engine re-run the charge after the crash?

Answer: Because result C sits in the durable log; replay skips logged steps and returns the old result.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · حالت‌های invocation: مسیر اصلی، retry، جبران، پایانهInvocation states: main path, retry, compensation, terminal · ماشین وضعیتstate machineباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
یک invocation سفارش از Triggered تا Done یا Compensated. نوار main را چپ‌به‌راست بخوان، بعد انحراف‌ها را.
One order invocation from Triggered to Done or Compensated. Read the main lane left to right, then the exits.
  1. مسیر اصلی پنج حالت دارد: Triggered، Running، Result Logged، Version Gate، Done — همه در نوار main.
  2. کرش از Running به Retrying می‌رود (همان ستون) و با همان شناسه برمی‌گردد؛ پایان بودجه یعنی Failed.
  3. دروازه نسخه تاریخچه قدیمی را به کد قدیمی میخ می‌کند؛ بن‌بست به Compensating و بعد Compensated پایانه می‌رسد.
  1. The main path holds five states: Triggered, Running, Result Logged, Version Gate, Done — all on the main lane.
  2. A crash leaves Running for Retrying (same column) and returns under the same id; a spent budget means Failed.
  3. The version gate pins old history to old code; a dead end reaches Compensating, then terminal Compensated.

نقطهٔ تصمیم: دو نقطه تصمیم: دروازه نسخه (کدام کد قدم می‌زند؟) و خروج از Running (retry یا جبران؟).

رد خرابی: جبران ناموفق در همان حلقه retry می‌ماند تا بودجه؛ invocation «منتظر اقدام» می‌شود، نه رها.

تضمین نشان‌داده‌شده: شکل نشان می‌دهد هر invocation دقیقاً در یک پایانه می‌ایستد: Done، Failed یا Compensated.

تضمین نشان‌نداده‌شده: ترتیب پیام‌های واقعی روی سیم را نشان نمی‌دهد — آن کار نمودار توالی است.

Decision point: Two decision points: the version gate (which code walks?) and the exit from Running (retry or compensate?).

Failure trace: A failed compensation stays in the retry loop until the budget; the invocation becomes 'awaiting action', never abandoned.

Guarantee shown: The figure shows every invocation ends in exactly one terminal: Done, Failed or Compensated.

Not shown: It does not show the actual message order on the wire — that is the sequence diagram's job.

خودآزماییSelf-check

چرا Compensated در نوار terminal است ولی Done در main؟

پاسخ: هر دو پایانه‌اند ولی معنای متفاوت: Done موفقیت مسیر اصلی است، Compensated پایان مسیر جبران پس از بن‌بست.

Why is Compensated on the terminal lane while Done stays on main?

Answer: Both are terminal but mean different things: Done is main-path success, Compensated ends the compensation path after a dead end.

نمودار اسمبل نهاییFinal assembly diagram · گردش‌کار پایدار سفارش: گام‌ها، retry، جبران، دروازه نسخهDurable order workflow: steps, retry, compensation, version gate · ماشین وضعیتstate machineباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
همان سفارش ۱۰۰۱ روی یک صفحه: پنج گام main، نوار retry، جبران و دروازه نسخه. از Start شروع کن.
The same order 1001 on one page: five main steps, the retry strip, compensation and the version gate. Start at Start.
  1. سطر main: پنج جعبه با guard روی هر یال — فقط گام ثبت‌شده جلو می‌رود.
  2. نوار آبی: قانون retry — همان شناسه، استفاده از نتیجه ثبت‌شده.
  3. پایین: دروازه نسخه (بازپخش v1 با کد v1) و مسیر جبران به Compensated پایانه.
  4. دو سطر پانوشت: تغییرها نسخه تازه می‌خواهند؛ بدون کلید ارسال، retry کور ممنوع و مغایرت‌گیری لازم.
  1. Main row: five boxes with a guard on each edge — only a logged step advances.
  2. Blue strip: the retry rule — same step id, reuse of the logged result.
  3. Below: the version gate (v1 replay on v1 code) and the compensation path to terminal Compensated.
  4. Two footer lines: changes need a new version; without a ship key, blind retry is banned and reconciliation is required.

نقطهٔ تصمیم: نقطه تصمیم هر یال است: guard برقرار و نتیجه ثبت شده؟

رد خرابی: کرش میان اثر و لاگ را روی فلش ship دنبال کن: گام باز می‌ماند، دوباره با همان شناسه می‌رود.

تضمین نشان‌داده‌شده: شکل کل استدلال را یک‌جا جمع می‌کند: شناسه، لاگ، جبران، نسخه.

تضمین نشان‌نداده‌شده: زمان‌بندی دقیق کرش و ترتیب پیام‌ها را نشان نمی‌دهد — آن‌ها در دو نمودار گام‌اند.

Decision point: The decision point is every edge: guard holds and result logged?

Failure trace: Trace a crash between effect and log on the ship arrow: the step stays open and goes again under the same id.

Guarantee shown: The figure gathers the whole argument on one page: ids, log, compensation, versions.

Not shown: It does not show exact crash timing or message order — those live in the two step diagrams.

خودآزماییSelf-check

کدام دو جمله پانوشت، دو پیگیری مصاحبه را جواب می‌دهند؟

پاسخ: «تغییرها نسخه تازه» (نسخه‌بندی invocationهای در جریان) و «بدون کلید، retry کور ممنوع» (تضمین ضعیف‌شده ارسال).

Which two footer lines answer the two interview follow-ups?

Answer: 'Changes need a new version' (versioning in-flight invocations) and 'no key, no blind retry' (the weakened shipping guarantee).

گزینه‌های جایگزینAlternatives

  • کوروگرافی رویدادی بدون orchestrator: هر سرویس به رویداد قبلی واکنش نشان می‌دهد. هزینه: تاریخچه پراکنده، بازپخش و نسخه‌بندی سخت‌تر، جبران بدون مالک واحد.
  • Event choreography with no orchestrator: each service reacts to the previous event. Cost: scattered history, harder replay and versioning, compensation with no single owner.

سناریوهای خرابیFailure scenarios

  • کرش میان اثر ship و نوشتن لاگ: ship دوباره اجرا می‌شود. با تکراری‌زدایی روی شناسه، مرسوله دوم ساخته نمی‌شود؛ بدون کلید، جاروی مغایرت‌گیری مرسوله‌های بی‌صاحب را به سفارش وصل می‌کند.
  • بازپخش تاریخچه v1 با کد v2 جابه‌جاشده: دروازه نسخه جلوی قدم‌زدن اشتباه را می‌گیرد و invocation را به کد v1 میخ می‌کند.
  • شکست خود جبران (مثلاً refund ناموفق): جبران retry می‌شود با همان شناسه؛ پس از پایان بودجه، هشدار انسانی با حالت دقیق لاگ — invocation نه «نیمه‌تمام رها» بلکه «منتظر اقدام» است.
  • Crash between the ship effect and the log write: ship re-runs. With dedup on the step id no second parcel is made; without a key, the reconciliation sweep attaches orphan parcels to the order.
  • Replaying v1 history against reordered v2 code: the version gate blocks the wrong walk and pins the invocation to v1 code.
  • The compensation itself fails (e.g. refund rejected): it retries under the same id; after the budget, a human alert with the exact log state — the invocation is 'awaiting action', never 'abandoned halfway'.

کد قابل اجراRunnable code

اجراشده و تأییدشده در همین محیطExecuted and verified in this environment

این کد آموزشی و ساده‌سازی‌شده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچ‌کس این‌شکلی کد پروداکشن نمی‌زند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.function.Supplier;

public class WorkflowDemo {
    // Durable log: step id -> result. Teaching stand-in for a write-ahead log.
    static Map<String, String> log = new LinkedHashMap<>();

    static String runStep(String id, Supplier<String> fn) {
        if (log.containsKey(id)) {
            System.out.println("replay " + id + ": skip, reuse " + log.get(id));
            return log.get(id);
        }
        String r = fn.get();
        log.put(id, r); // append BEFORE reporting success
        System.out.println("exec   " + id + ": " + r + " (logged)");
        return r;
    }

    public static void main(String[] args) {
        String wid = "order-1001";
        runStep(wid + ":reserve:v1", () -> "stock-42 reserved");
        runStep(wid + ":charge:v1", () -> "pay-7 charged");
        System.out.println("-- crash before ship; restart replays --");
        runStep(wid + ":reserve:v1", () -> "stock-42 reserved");
        runStep(wid + ":charge:v1", () -> "pay-7 charged");
        runStep(wid + ":ship:v1", () -> "parcel-9 created");
        System.out.println("-- duplicate delivery of ship result --");
        runStep(wid + ":ship:v1", () -> "parcel-9 created");
        System.out.println("durable log keys: " + new TreeSet<>(log.keySet()));
    }
}

اجرا: javac WorkflowDemo.java && java WorkflowDemo — فقط استاندارد جاوا (JDK 27)، فوری. فرایند را ذهنی پیش از خط ship بکش: خط‌های بازپخش دقیقاً همان چیزی است که restart چاپ می‌کند.

خروجی مورد انتظار: اجرای reserve و charge با ثبت لاگ؛ کرش پیش از ship؛ بازپخش هر دو با استفاده مجدد از نتیجه ثبت‌شده؛ اجرای ship با ثبت؛ تحویل تکراری ship با استفاده مجدد؛ کلیدهای لاگ پایدار: هر سه گام نسخه v1.

Run: javac WorkflowDemo.java && java WorkflowDemo — Java stdlib only (JDK 27), instant. Kill the process mentally before the ship line: the replay lines are exactly what a restart prints.

Expected output: exec order-1001:reserve:v1: stock-42 reserved (logged); exec order-1001:charge:v1: pay-7 charged (logged); -- crash before ship; restart replays --; replay order-1001:reserve:v1: skip, reuse stock-42 reserved; replay order-1001:charge:v1: skip, reuse pay-7 charged; exec order-1001:ship:v1: parcel-9 created (logged); -- duplicate delivery of ship result --; replay order-1001:ship:v1: skip, reuse parcel-9 created; durable log keys: [order-1001:charge:v1, order-1001:reserve:v1, order-1001:ship:v1].

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

پیش و پس از هر نوشتن لاگ کرش تزریق کن و مطمئن شو هیچ گامی دو بار اثر نمی‌گذارد؛ نتیجه هر گام را تکراری تحویل بده و استفاده مجدد را ببین؛ تاریخچه قدیمی را با کد جابه‌جاشده بازپخش کن و ببین دروازه نسخه آن را به نسخه قدیمی میخ می‌کند؛ هر جبران را اجرا کن و حالت پایانی را ببین. lag بازپخش، شمارنده اثر تکراری و عمق صف مغایرت‌گیری را بپا.
Crash-inject before and after each log append and confirm no step runs twice with effect; duplicate-deliver every step result and confirm reuse; replay an old history against reordered code and confirm the version gate pins it to the old version; run every compensation and confirm the end state. Track replay lag, duplicate-effect counter and reconciliation queue depth.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (استف): provider ارسال idempotency key ندارد؛ تضمینت چه تغییری می‌کند؟Follow-up (Staff): The shipping provider has no idempotency key. How does that change your guarantee?

تضمین از «اثر دقیقاً یک‌بار» به «حداکثر یک مرسوله بی‌صاحب + مغایرت‌گیری» می‌لغزد: retry کور ممنوع، چون هر تلاش ممکن است مرسوله تازه بسازد. به‌جایش: ship را یک بار صدا بزن، نتیجه نامشخص را «نامعلوم» ثبت کن، و جاروی مغایرت‌گیری (تطبیق مرسوله‌ها با شناسه سفارش در سمت خودت) اثر را قطعی کند. هزینه recovery همان جاروست.

The guarantee slides from 'exactly-once effects' to 'at most one orphan parcel + reconciliation': blind retries are banned, since each attempt may mint a fresh parcel. Instead: call ship once, record an unclear outcome as 'unknown', and let a reconciliation sweep (matching parcels to your order id on your side) settle the effect. Recovery cost is that sweep.

اشتباهات رایجCommon mistakes

  • قول «exactly-once» برای اثر بیرونی. لاگ پایدار اجرای دقیقاً یک‌بار می‌دهد، نه اثر دقیقاً یک‌بار — اثر به کلید مقصد وابسته است.
  • استفاده از عدد تصادفی یا ساعت سیستم در کد workflow. بازپخش باید قطعی باشد؛ همان ورودی، همان قدم‌ها.
  • جابه‌جایی آزاد ترتیب گام‌ها روی invocationهای در جریان. تاریخچه قدیمی فقط با منطق قدیمی قدم زده می‌شود — تغییر، نسخه تازه می‌خواهد.
  • Promising 'exactly-once' for outside effects. The durable log delivers exactly-once execution, not exactly-once effects — effects depend on the destination's key.
  • Using random numbers or the system clock in workflow code. Replay must be deterministic; same input, same walk.
  • Freely reordering steps over in-flight invocations. Old history is only ever walked by old logic — change needs a new version.

منابع:Sources: AWS · safe retries with idempotent APIs · Atlassian · engineering interview handbook