DDIA·مطالعهSTUDY

نقشه راهRoadmap/ فصل ۵Chapter 5

کدگذاری و تکاملEncoding and evolution

داده برای خروج از حافظهٔ برنامه باید به قالبی قابل‌ذخیره یا قابل‌ارسال تبدیل شود. مسئلهٔ اصلی این فصل این است که پس از تغییرِ قالب، نسخه‌های قدیم و جدید چگونه همچنان با هم کار کنند. How data becomes bytes, how old and new versions coexist, and why backward/forward compatibility is the backbone of zero-downtime deploys — with 6 interactive (Archify) and 3 concept diagrams, a teacher narration for each, and an interview question pack.

۹9 نمودار (۶ تعاملی + ۳ مفهومی)diagrams (6 interactive + 3 concept)· محور: compatibility · protobuf · Avro · dataflow modesTheme: compatibility · protobuf · Avro · dataflow modes· زمان مطالعه: حدود ۶۰ دقیقهReading time: ~60 min

۰0نقشهٔ راه فصلChapter roadmap

ایدهٔ کلیدی فصلChapter thesis

داده برای خروج از حافظهٔ برنامه باید به قالبی قابل‌ذخیره یا قابل‌ارسال تبدیل شود. مسئلهٔ اصلی این فصل این است که پس از تغییرِ قالب، نسخه‌های قدیم و جدید چگونه همچنان با هم کار کنند.

مثال فنی: رکورد قدیمی زیر کد جدید

وضعیت اولیه: نسخهٔ ۱ رکورد {name: "Sara"} نوشته و نسخهٔ ۲ فیلد email اضافه کرده است.

اجرای مرحله‌به‌مرحله: خوانندهٔ تازه برای رکورد قدیمی باید رفتارِ نبود email را بداند؛ خوانندهٔ قدیمی هم باید فیلد تازه را طبق قرارداد تحمل کند.

نتیجه و دلیل: آزمون سازگاری باید هر دو جهت خواندن را پوشش دهد؛ صرفِ موفقیت encode/decode با نسخهٔ یکسان کافی نیست.

مثال‌های این فصل سناریوهای آموزشیِ برگرفته از کارهای روزمره‌اند. عددهای انتخاب‌شده برای توضیح‌اند؛ معماریِ داخلیِ یک شرکت مشخص را توصیف نمی‌کنند.

Whenever data crosses a process boundary — written to disk, sent over the network, or handed to the next version of the program itself — it must be encoded from “in-memory structures” into a sequence of bytes. This chapter isn’t about the details of that conversion; it’s about its architectural consequences: if you change the byte layout, who can no longer read the data?

چون deploy در سیستم‌های بزرگ تدریجی است (rolling upgrade) و کلاینت‌ها را نمی‌توان مجبور به به‌روزرسانی کرد، نسخه‌های قدیم و جدیدِ کد هم‌زمان اجرا می‌شوند و داده‌های قدیم و جدید کنار هم می‌مانند. نتیجه: هر قالبِ داده باید هم backward باشد (کدِ جدید، دادهٔ قدیمی را بخواند) و هم forward (کدِ قدیمی، دادهٔ جدید را نابود نکند).

Because deploys in large systems are gradual (rolling upgrades) and clients can’t be forced to update, old and new versions of the code run side by side while old and new data sit next to each other. So every data format must be both backward compatible (new code reads old data) and forward compatible (old code doesn’t destroy new data).

این فصل دو نیمهٔ مشخص دارد:

This chapter has two distinct halves:

  • نیمهٔ اول — قالب‌های کدگذاری: از قالب‌های ویژهٔ زبان (مثل java.io.Serializable) تا JSON/XML/CSV و بعد قالب‌های باینریِ مبتنی بر schema مثل Protocol Buffers و Avro. هرکدام قواعد تکامل (schema evolution) متفاوتی دارند.
  • نیمهٔ دوم — حالت‌های جریان داده (modes of dataflow): داده بین processها چطور حرکت می‌کند؟ از طریق پایگاه داده (نوشتن = پیام به آیندهٔ خودت)، از طریق سرویس‌ها (REST/RPC)، از طریق workflow engine‌ها (اجرای پایدار)، یا از طریق message broker و actorها (معماری event-driven). هر حالت، ترکیب متفاوتی از سازگاریِ backward/forward را لازم دارد.
  • First half — encoding formats: from language-native formats (like java.io.Serializable) through JSON/XML/CSV to schema-driven binary formats like Protocol Buffers and Avro. Each has different schema-evolution rules.
  • Second half — dataflow modes: how does data move between processes? Via a database (writing = messaging your future self), via services (REST/RPC), via workflow engines (durable execution), or via a message broker and actors (event-driven architecture). Each mode needs a different mix of backward/forward compatibility.
نکتهٔ مصاحبهInterview tip
جملهٔ طلایی این فصل: «data outlives code» — کد در چند دقیقه deploy می‌شود، اما دادهٔ پنج‌ساله هنوز با همان فرمتِ روز اول روی دیسک است. هر تصمیمِ قالبِ داده، تعهدی بلندمدت‌تر از عمرِ هر نسخهٔ کد است.

This chapter’s golden line: “data outlives code” — code deploys in minutes, but five-year-old data still sits on disk in its day-one format. Every data-format decision is a longer-term commitment than any code version’s lifetime.

۱1encode و decodeEncoding and decoding

کدگذاری یعنی تبدیل دادهٔ داخل برنامه به بایت‌هایی که بتوان ذخیره یا ارسال کرد. رمزگشاییِ قالب یا decode، همان بایت‌ها را دوباره به دادهٔ قابل‌استفاده تبدیل می‌کند؛ این مفهوم با رمزنگاری امنیتی فرق دارد.

مثال فنی: شیء با بایت فرق دارد

وضعیت اولیه: برنامه شیئی با user_id=17 و name=Sara در حافظه دارد.

اجرای مرحله‌به‌مرحله: encoder آن را به بایت تبدیل می‌کند؛ decoder با قواعد همان قالب، فیلدها را از بایت‌ها بازسازی می‌کند.

نتیجه و دلیل: اشاره‌گرهای حافظه و کلاس‌های زبان به‌خودی‌خود قراردادِ قابل‌انتقال نیستند؛ قالب باید معنی فیلدها را مشخص کند.

Programs hold data in two shapes:

  • درون حافظه: object، struct، لیست، آرایه، hash table، درخت — ساختارهایی که برای دسترسیِ سریعِ CPU (معمولاً با pointer) بهینه‌اند.
  • روی دیسک / شبکه: باید دنباله‌ای خوداتکا از بایت‌ها باشد (مثل یک سند JSON)؛ چون pointer برای process دیگر معنایی ندارد.
  • In memory: objects, structs, lists, arrays, hash tables, trees — structures optimized for fast CPU access (usually with pointers).
  • On disk / network: must be a self-contained byte sequence (like one JSON document); a pointer means nothing to another process.
ترجمهٔ از نمایشِ درون‌حافظه‌ای به بایت‌ها یعنی encoding (معادل‌های رایج: serialization یا marshaling) و ترجمهٔ معکوس یعنی decoding (parsing، deserialization یا unmarshaling).

Translating from the in-memory representation to bytes is encoding (common synonyms: serialization, marshaling); the reverse is decoding (parsing, deserialization, unmarshaling).

دامِ اصطلاحTerminology trap
واژهٔ serialization در بحث تراکنش‌ها (فصل ۸) معنای کاملاً متفاوتی دارد (ترتیب‌پذیریِ اجرا). کتاب برای جلوگیری از ابهام، همه‌جا encoding می‌گوید.

The word serialization means something completely different in transaction discussions (Chapter 8: execution orderability). To avoid ambiguity, the book says encoding everywhere.

  • گاهی encode/decode لازم نیست: مثلاً موتوری که مستقیم روی دادهٔ فشردهٔ خوانده‌شده از دیسک کار می‌کند (بخش Query Execution در فصل ۴)، یا قالب‌های zero-copy مثل Cap'n Proto و FlatBuffers که همان بایت‌ها هم‌زمان نمایِ اجرا و نمایِ ذخیره‌سازی‌اند و تبدیلِ جداگانه ندارند.
  • اما اکثر سیستم‌ها به تبدیلِ بین اشیای درون حافظه و دنبالهٔ بایت نیاز دارند — و انتخابِ قالب، فقط بحثِ اندازه و سرعت نیست؛ بحثِ تکامل‌پذیری است.
  • Sometimes no encode/decode is needed: e.g. an engine working directly on compressed data read from disk (the Query Execution part of Chapter 4), or zero-copy formats like Cap’n Proto and FlatBuffers where the same bytes are both the execution and the storage view, with no separate conversion.
  • But most systems must convert between in-memory objects and byte sequences — and the format choice isn’t just about size and speed; it’s about evolvability.

۲2سازگاری و ارتقای تدریجیCompatibility and gradual upgrades

در ارتقای تدریجی، نسخه‌های قدیم و جدید هم‌زمان کار می‌کنند. سازگاری عقب‌رو یعنی کد جدید دادهٔ قدیم را بخواند؛ سازگاری جلو‌رو یعنی کد قدیم بتواند با دادهٔ جدید طبق قرارداد کنار بیاید.

مثال فنی: چهار ترکیب در rolling upgrade

وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.

اجرای مرحله‌به‌مرحله: نسخهٔ ۲ رکورد قدیمی را می‌خواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.

نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش می‌دهد.

Changes in program features usually mean changes in the data it stores: a fresh field, a fresh record type, or a fresh rendering of old data. But code never changes in one instant:

  • در سمت سرور: rolling upgrade (یا staged rollout) — نسخهٔ جدید چند node به چند node deploy می‌شود، پایش می‌شود و جلو می‌رود. بدون downtime، با releaseهای کوچک و مکرر.
  • در سمت کلاینت: کاربر ممکن است مدت‌ها اپ را به‌روز نکند؛ کنترل دست تو نیست.
  • Server side: rolling upgrades (or staged rollouts) — the new version deploys node by node, gets monitored, and moves on. No downtime, with small frequent releases.
  • Client side: users may not update the app for a long time; that timing is out of your hands.
پس نسخه‌های قدیم و جدیدِ کد و قالب‌های قدیم و جدیدِ داده باید کنار هم زندگی کنند:

So old and new versions of the code and old and new versions of the data formats must live side by side:

نوع سازگاریتعریفیادآوری
Backward compatibilityکدِ جدیدتر بتواند داده‌ای را که کدِ قدیمی‌تر نوشته بخواند«از گذشته خواندن»
Forward compatibilityکدِ قدیمی‌تر بتواند داده‌ای را که کدِ جدیدتر نوشته بخواند«از آینده خواندن» — سخت‌تر، چون کدِ قدیم باید افزوده‌های ناشناخته را نادیده اما حفظ کند
Compatibility typeDefinitionMnemonic
Backward compatibilityNewer code can read data written by older code“Reading from the past”
Forward compatibilityOlder code can read data written by newer code“Reading from the future” — harder, since old code must ignore unknown additions but preserve them
در APIها دقیقاً چه چیزی سازگار می‌خواهد؟What exactly needs compatibility in APIs?
اگر کلاینتِ قدیمی باید سرویسِ جدید را صدا بزند: روی request سازگاریِ backward و روی response سازگاریِ forward لازم است. اگر کلاینتِ جدید با سرویسِ قدیمی حرف بزند، برعکس. backward معمولاً آسان است (فرمتِ قدیم را می‌شناسی)؛ forward سخت‌تر است چون کدِ قدیم باید چیزهای ناشناخته را بی‌صدا کنار بگذارد.

If an old client must call a new service: requests need backward compatibility, responses need forward compatibility. A new client talking to an old service is the mirror image. Backward is usually easy (you know the old format); forward is harder since old code must silently set aside unknown things.

نمودار مفهومی — ماتریس سازگاری: چه کسی چه چیزی را می‌خواند؟Concept diagram — the compatibility matrix: who reads what?

نمودار مفهومی · backward در برابر forwardConcept diagram · Backward vs forward باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: چهار ترکیب در rolling upgrade

وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.

اجرای مرحله‌به‌مرحله: نسخهٔ ۲ رکورد قدیمی را می‌خواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.

نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش می‌دهد.

جدول دو در دو را بخوانید: سطرها «دادهٔ قدیمی» و «دادهٔ جدید»، ستون‌ها «کد قدیمی می‌خواند» و «کد جدید می‌خواند». خانهٔ «دادهٔ قدیمی + کد جدید» همان سازگاری backward است (جدید، قدیم را می‌خواند) و خانهٔ «دادهٔ جدید + کد قدیمی» همان forward (قدیم، جدید را می‌خواند و نگه می‌دارد). دو خانهٔ هم‌نسخه بی‌دردسرند. نوار پایین حکم ارتقای تدریجی است: چون نسخه‌ها قاطی‌اند، هر دو سازگاری لازم است.

Read the 2×2 grid: rows “Old data” and “New data”, columns “Old code reads” and “New code reads”. “Old data + new code” is backward compatibility (the new reads the old); “New data + old code” is forward (the old reads and preserves the new). The same-version cells are painless. The bottom bar states the rolling-upgrade rule: with mixed versions live, you need BOTH.

۲.۱ نمودار ۱ + روایت مدرس2.1 Diagram 1 — rolling upgrades and unknown-field loss

خطرِ معروفِ شکل ۵-۱ کتاب: کدِ قدیمی رکوردی را که کدِ جدید با فیلدِ تازه نوشته می‌خواند، به‌روز می‌کند و دوباره می‌نویسد — و اگر decoder عمداً فیلدهای ناشناخته را نگه ندارد، آن فیلد برای همیشه می‌رود.

The famous danger of the book’s Figure 5-1: old code reads a record the new code wrote with a fresh field, updates it, writes it back — and if the decoder doesn’t deliberately keep unknown fields, that field is gone forever.

نمودار ۱ · Sequence — ارتقای تدریجی و خطر گم شدن فیلد ناشناختهDiagram 1 · Sequence — rolling upgrades and unknown-field loss باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۱Teacher narration

مثال فنی برای خواندنِ نمودار: چهار ترکیب در rolling upgrade

وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.

اجرای مرحله‌به‌مرحله: نسخهٔ ۲ رکورد قدیمی را می‌خواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.

نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش می‌دهد.

سه شرکت‌کننده داریم که از چپ به راست نشسته‌اند: «Service v2.2» (زیرنویس: «new code, new field» — نسخهٔ جدیدِ کد)، وسط «Record Store» (زیرنویس: «shared database»)، و سمت راست «Service v2.1» (زیرنویس: «old code still live» — همان نسخهٔ قدیمی که هنوز زنده است). زمان از بالا به پایین می‌گذرد و سه نوارِ عنوان در سمت چپ، سه پردهٔ داستان را جدا می‌کنند.

Three participants, left to right: Service v2.2 (new code, new field), Record Store (shared database), and Service v2.1 (old code still live). Time flows downward through three titled bands.

پردهٔ اول، «Rolling upgrade»: فلشِ پررنگِ «write record with new field» از v2.2 به Record Store می‌رود. بعد فلشِ معمولیِ «read record» از v2.1 (هنگام ارتقای تدریجی، node قدیمی هنوز سرویس می‌دهد) و پاسخِ برگشتیِ «record incl. unknown field» — یعنی رکوردِ حاویِ فیلدِ ناشناخته به دستِ کدِ قدیمی رسید.

Act one, Rolling upgrade: bold write record with new field from v2.2 to the store; then plain read record from v2.1 (during gradual rollout the old node still serves) with the reply record incl. unknown field — the unknown-field record in old code’s hands.

پردهٔ دوم، «Unknown-field hazard»: فلشِ پررنگِ «update known fields, write back» از v2.1 به Store — و یادداشتِ زیرِ همان فلش می‌گوید «decoded into a plain model object» (رکورد در object ساده‌ای decode شده که فیلدِ ناشناخته را نگه نمی‌دارد). v2.2 دوباره می‌خواند («read record again») و پاسخِ برگشتی، تلخ است: «new field gone — data lost» با یادداشتِ «unknown field was not preserved». این همان گم‌شدنِ بی‌صدای شکل ۵-۱ است.

Act two, Unknown-field hazard: bold update known fields, write back from v2.1 to the store — annotated decoded into a plain model object (the record decoded into a plain object that keeps no unknown field). v2.2 reads again (read record again) and the reply is bitter: new field gone — data lost (unknown field was not preserved). That is Figure 5-1’s silent loss.

پردهٔ سوم، «Preserving unknown fields»: فلشِ چین‌چینِ «write record with field tag 4» — یعنی رکورد با شمارهٔ تگِ فیلد کد می‌شود نه با نام. کدِ قدیمی رکورد را round-trip می‌کند («old code round-trips the record») و این‌بار پاسخِ برگشتی خبر خوب می‌دهد: «field tag 4 still intact».

Act three, Preserving unknown fields: the dashed write record with field tag 4 — the record encoded with a field tag number, not a name. Old code round-trips the record (old code round-trips the record) and this time the reply is good news: field tag 4 still intact.

درسِ شکل: مشکل از «قدیمی بودنِ کد» نیست؛ از قراردادِ decoder است. قالب‌های مبتنی بر schema (مثل Protocol Buffers و Avro) به کدِ قدیمی می‌گویند چیزهای ناشناخته را نادیده اما بایت‌هایشان را دست‌نخورده نگه دارد.

The lesson: the problem isn’t “old code”; it’s the decoder contract. Schema-driven formats (like Protocol Buffers and Avro) tell old code to ignore unknown things but keep their bytes untouched.

۳3قالب‌های کدگذاریEncoding formats

قالبِ تبادل مشخص می‌کند داده چگونه نمایش داده شود و خواننده از کجا ساختارش را بفهمد. انتخاب قالب، بر حجم، خوانایی، ارتباط میان زبان‌ها و امکان تغییر آینده اثر دارد.

مثال فنی: مقایسهٔ قالب روی همان رکورد

وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب می‌نویسیم.

اجرای مرحله‌به‌مرحله: JSON نام فیلدها را متنی حمل می‌کند؛ قالب مبتنی بر schema می‌تواند شناسه یا جایگاه فیلد را به‌کار گیرد.

نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن به‌تنهایی تضمین کمترین حجم یا بهترین تکامل نیست.

Now that we know which compatibilities are needed, what are the options? This part surveys three families: language-native formats, textual formats (JSON/XML/CSV), and schema-driven binary formats (Protocol Buffers and Avro).

۳.۱ نمودار ۲ + روایت مدرس3.1 Diagram 2 — the encoding-format decision tree

نمودار ۲ · Workflow — درخت تصمیم انتخاب قالب کدگذاریDiagram 2 · Workflow — the encoding-format decision tree باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۲Teacher narration

مثال فنی برای خواندنِ نمودار: مقایسهٔ قالب روی همان رکورد

وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب می‌نویسیم.

اجرای مرحله‌به‌مرحله: JSON نام فیلدها را متنی حمل می‌کند؛ قالب مبتنی بر schema می‌تواند شناسه یا جایگاه فیلد را به‌کار گیرد.

نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن به‌تنهایی تضمین کمترین حجم یا بهترین تکامل نیست.

این نمودار چهار نوار افقی دارد که از بالا به پایین عنوان‌شان هست: «Decision» (پرسش‌ها)، «Language-Native» (که با پس‌زمینهٔ استثنا مشخص شده)، «Textual Formats» و «Binary Schema-Driven». در بالا هم دو بخشِ فاز داریم: «Scope» روی دو ستونِ اول و «Choose the format» روی بقیهٔ ستون‌ها.

The chart has four horizontal lanes, top to bottom: Decision (questions), Language-Native (marked as the exception), Textual Formats, Binary Schema-Driven — with two phase bands above: Scope over the first two columns, Choose the format over the rest.

مسیرِ اصلی از جعبهٔ «Record to Encode» (زیرنویس: «object to bytes») شروع می‌شود؛ فلشِ پررنگِ «encode once» آن را به اولین پرسش می‌رساند: «Transient + One Language?» (زیرنویس: «in-process handoff only»، برچسبِ decide).

The main path starts at Record to Encode (object to bytes); the bold encode once reaches the first question: Transient + One Language? (in-process handoff only).

از هر پرسش دو فلش بیرون می‌آید. فلشِ «بله» از پرسشِ اول مستقیم پایین می‌آید به جعبهٔ «Language-Native» (زیرنویس: «Serializable / pickle»، برچسبِ هشدارِ «compat risk»)؛ فلشِ «خیر» به راست، به پرسشِ دوم: «Human-Readable?». از آن «بله» پایین می‌رود به «JSON / XML / CSV» (زیرنویس: «textual, schema optional»، برچسبِ «JSON Schema for validation») و «خیر» به راست به پرسشِ سوم: «Schema-Driven Binary?».

Each question has two exits. “Yes” from the first drops straight to Language-Native (Serializable / pickle; warning tag compat risk); “no” goes right to the second question Human-Readable?. Its “yes” drops to JSON / XML / CSV (textual, schema optional; JSON Schema for validation), its “no” right to the third question Schema-Driven Binary?.

از پرسشِ سوم، فلشِ «tag-based» پایین می‌آید و از کنارِ چپ به «Protocol Buffers» می‌رسد (زیرنویس: «field tags in the data»، برچسبِ «Thrift similar») و فلشِ پررنگِ «name-based» به راست می‌پیچد و به «Avro» می‌رسد (زیرنویس: «writer and reader schemas»، برچسبِ «no tag numbers»). این دو جعبه داخل قابِ «Defined evolution rules» جمع شده‌اند — یعنی پاداشِ انتخابِ این مسیر.

From the third question, tag-based drops down-left to Protocol Buffers (field tags in the data; Thrift similar), and bold name-based turns right to Avro (writer and reader schemas; no tag numbers). Both sit inside Defined evolution rules — the reward for taking this path.

درسِ شکل: ترتیبِ پرسش‌ها مهم است. اول طول عمر و مخاطبِ بایت‌ها را مشخص کن (موقت و تک‌زبانه؟)، بعد انسان‌خوان بودن، و در آخر قواعد تکامل. هر «بله» شما را به خانواده‌ای می‌رساند و جزئیاتش را در بخش‌های بعد می‌بینیم.

The lesson: question order matters. First pin down the bytes’ lifespan and audience (transient and single-language?), then human-readability, finally evolution rules. Each “yes” lands you in one family, detailed in the sections ahead.

۳.۲ قالب‌های ویژهٔ زبان3.2 Language-native formats

قالب‌های ویژهٔ زبان، ذخیرهٔ اشیای همان زبان را آسان می‌کنند، اما تبادل میان زبان‌ها و نسخه‌ها را دشوارتر می‌کنند. برخی روش‌های بازسازی شیء برای ورودی نامطمئن خطرناک‌اند.

مثال فنی: نام کلاس، قرارداد پایدار نیست

وضعیت اولیه: فایل ذخیره‌شده به کلاس OldUser در نسخهٔ قبلی برنامه وابسته است.

اجرای مرحله‌به‌مرحله: نسخهٔ جدید کلاس را حذف کرده یا زبان دیگری دارد؛ decoder ویژهٔ زبان ممکن است نتواند شیء را بسازد.

نتیجه و دلیل: داده از عمر یک نسخهٔ کد طولانی‌تر است؛ برای تبادل، schema مستقل و ورودیِ معتبر لازم است.

  • اکثر زبان‌ها کتابخانهٔ کدگذاریِ داخلی دارند: java.io.Serializable در Java، pickle در Python، Marshal در Ruby و کتابخانه‌هایی مثل Kryo. راحت‌اند: object را با کمترین کد ذخیره و بازیابی می‌کنند.
  • اما چهار مشکلِ بنیادین دارند:
  • قفلِ زبانی: کدگذاری به یک زبان گره خورده؛ خواندن داده با زبانِ دیگر دشوار است و یکپارچه‌سازی با سازمان‌های دیگر را ناممکن می‌کند.
  • امنیت: برای بازسازیِ objectها، decoder باید بتواند class دلخواه instantiate کند — اگر مهاجم بایت‌های دلخواه بفرستد، معمولاً به اجرای کدِ دلخواه (RCE) می‌رسد.
  • نسخه‌بندیِ فرعی: این کتابخانه‌ها برای «سریع و آسان» ساخته شده‌اند و معمولاً به سازگاریِ forward/backward بی‌توجه‌اند.
  • کاراییِ فرعی: serialization داخلیِ Java به کندی و حجمِ متورم معروف است.
  • نتیجه: جز برای مقاصدِ بسیار موقت، از کدگذاریِ داخلیِ زبان استفاده نکن.
  • Most languages ship a built-in encoding library: java.io.Serializable in Java, pickle in Python, Marshal in Ruby, plus libraries like Kryo. They’re comfortable: store and restore objects with minimal code.
  • But they carry four fundamental problems:
  • Language lock-in: encoding is tied to one language; reading the data in another language is hard, and integrating with other organizations becomes impossible.
  • Security: to rebuild objects the decoder must instantiate arbitrary classes — if an attacker sends arbitrary bytes, arbitrary code execution (RCE) usually follows.
  • Versioning as an afterthought: these libraries are built for “fast and easy” and mostly ignore forward/backward compatibility.
  • Performance as an afterthought: Java’s built-in serialization is famously slow and bloated.
  • Verdict: never use language-internal encoding except for very transient purposes.

۳.۳ JSON، XML، CSV و JSON Schema3.3 JSON, XML, CSV and JSON Schema

JSON و XML ساختار نام‌دار دارند؛ CSV بیشتر برای جدول ساده است و جزئیاتش باید توافق شود. JSON Schema می‌تواند قواعدی مثل نوع فیلد و اجباری بودن آن را مشخص کند.

مثال فنی: شناسهٔ بزرگ در JSON

وضعیت اولیه: شناسهٔ 9007199254740993 به‌صورت JSON number به JavaScript می‌رسد.

اجرای مرحله‌به‌مرحله: تبدیل به Number ممکن است آن را به 9007199254740992 گرد کند؛ قراردادِ رشته‌ای برای شناسه، رقم‌ها را حفظ می‌کند.

نتیجه و دلیل: JSON به‌تنهایی دقتِ همهٔ خواننده‌ها را تضمین نمی‌کند؛ نوع فیلد باید در دو طرف قرارداد روشن باشد.

Standard multi-language formats: JSON and XML (plus CSV, which holds only tabular data and doesn’t nest). They’re textual and fairly human-readable, but their problems are real:

  • XML معمولاً طولانی و بیش از حد پیچیده نقد می‌شود.
  • ابهامِ اعداد: در XML و CSV اصلاً نمی‌توان عدد را از رشتهٔ عددی تشخیص داد (مگر با schema بیرونی). JSON رشته و عدد را جدا می‌کند اما integer و floating-point را نه و دقت هم مشخص نمی‌کند. عدد بزرگ‌تر از 2^53 در IEEE 754 دقیق نمی‌ماند؛ مثلاً X شناسهٔ ۶۴بیتیِ هر post را هم به‌صورت عدد JSON و هم رشتهٔ متنی برمی‌گرداند تا JavaScript خرابش نکند.
  • رشته‌های باینری ندارند: JSON/XML از Unicode (متنِ انسان‌خوان) پشتیبانی می‌کنند نه دنبالهٔ بایتِ خام. راه‌حلِ رایج Base64 است که کار را «تقریبی» می‌کند و حدود یک‌سوم حجم را زیاد می‌کند.
  • XML Schema و JSON Schema قدرتمند و در نتیجه پیچیده‌اند؛ برنامه‌ای که از آن‌ها استفاده نکند باید منطقِ کدگذاری/رمزگشایی را hardcode کند.
  • CSV اصلاً schema ندارد؛ معنای هر سطر و ستون را باید برنامه تعیین کند و escape کردنِ ویرگول و خط‌جدید هم مبهم است.
  • با این همه، برای تبادلِ بین سازمان‌ها همین‌ها «به‌اندازهٔ کافی خوب»‌اند: تا وقتی دو طرف بر سرِ قالب توافق کنند، زیبایی و کارایی اهمیت کمتری دارد.
  • XML is usually criticized as verbose and over-complex.
  • Number ambiguity: in XML and CSV you can’t tell a number from a numeric string at all (without an external schema). JSON separates strings from numbers but not integers from floats, and pins no precision. Numbers above 2^53 don’t survive IEEE 754 exactly; e.g. X returns each post’s 64-bit id both as a JSON number and as a text string so JavaScript won’t corrupt it.
  • No binary strings: JSON/XML support Unicode (human-readable text), not raw byte sequences. The common workaround is Base64, which roughly works and inflates size by about a third.
  • XML Schema and JSON Schema are powerful and hence complex; programs that skip them must hardcode the encode/decode logic.
  • CSV has no schema at all; the program must define each row and column’s meaning, and escaping commas and newlines is murky.
  • Still, for cross-organization exchange these are “good enough”: as long as both sides agree on the format, beauty and efficiency matter less.
JSON Schema در عملJSON Schema in practice
JSON Schema امروز در OpenAPI (سرویس‌های وب)، registryهایی مثل Confluent Schema Registry و Apicurio، و پایگاه داده‌ها (افزونهٔ pg_jsonschema در PostgreSQL و $jsonSchema در MongoDB) رایج است. علاوه بر نوع‌های پایه (string، number، integer، object، array، boolean، null) قواعد اعتبارسنجی هم دارد (مثلاً port بین ۱ تا ۶۵۵۳۵).

JSON Schema is common today in OpenAPI (web services), registries like Confluent Schema Registry and Apicurio, and databases (the pg_jsonschema extension in PostgreSQL, $jsonSchema in MongoDB). Beyond base types (string, number, integer, object, array, boolean, null) it has validation rules (e.g. a port between 1 and 65535).

نکتهٔ ظریف: content model باز در برابر بسته. باز (که additionalProperties: true پیش‌فرضش است) هر فیلدِ تعریف‌نشده‌ای را می‌پذیرد؛ یعنی JSON Schema معمولاً می‌گوید چه چیزی مجاز نیست تا اینکه بگوید چه چیزی مجاز است. مثال کتاب برای نقشهٔ «کلیدِ عددی → مقدارِ رشته‌ای»:

The subtle point: an open vs closed content model. Open (whose default is additionalProperties: true) accepts any undefined field; so JSON Schema usually says what is not allowed rather than what is. The book’s example for a “numeric-key → string-value” map:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "patternProperties": {
    "^[0-9]+$": { "type": "string" }
  },
  "additionalProperties": false
}
JSON Schema به شرط‌های if/else، نوع‌های نام‌دار و ارجاع به schemaهای دوردست هم مجهز است — قدرتمند، ولی تکامل‌دادنش به‌صورت سازگار کارِ دشواری است. XML Schema هم دغدغه‌های مشابه دارد.

JSON Schema also handles if/else conditions, named types and remote-schema references — powerful, but evolving it compatibly is hard work. XML Schema shares similar concerns.

باینری‌های «JSON» فقط نصفِ راه‌اند“JSON” binaries are only half the road
قالب‌هایی مثل MessagePack، CBOR و BSON همان مدلِ JSON را باینری می‌کنند (گاهی با چند نوعِ اضافه مثل رشتهٔ باینری). اما چون schema ندارند، هنوز نامِ همهٔ فیلدها داخل بایت‌هاست. مثالِ کتاب: رکوردِ سه‌فیلدیِ {"userName": "Martin", "favoriteNumber": 1337, "interests": [...]} با MessagePack ۶۶ بایت می‌شود در برابر ۸۱ بایتِ JSON متنیِ بدون فاصله — صرفه‌جوییِ اندک در برابر ازدست‌دادنِ انسان‌خوانی. در بخش‌های بعد همین رکورد را با Protocol Buffers (۳۳ بایت) و Avro (۳۲ بایت) می‌بینیم.

Formats like MessagePack, CBOR and BSON binarize the same JSON model (sometimes with a few extra types like binary strings). But schema-less, every field name still sits inside the bytes. The book’s example: the three-field record {"userName": "Martin", "favoriteNumber": 1337, "interests": [...]} becomes 66 bytes in MessagePack vs 81 bytes of spaceless textual JSON — a small saving for the loss of human-readability. Ahead we’ll see the same record at 33 bytes (Protocol Buffers) and 32 (Avro).

۳.۴ Protocol Buffers3.4 Protocol Buffers

در Protocol Buffers، فیلدها شماره دارند و همین شماره بخشی از هویت آن‌ها روی شبکه است. تغییر نام با استفادهٔ دوباره از شمارهٔ یک فیلد حذف‌شده یکسان نیست.

مثال فنی: Tag هویت فیلد است

وضعیت اولیه: schema پیام شامل string name = 1 است و بعد string email = 2 اضافه می‌شود.

اجرای مرحله‌به‌مرحله: شمارهٔ ۱ همچنان به name تعلق دارد؛ پس از حذف email، شمارهٔ ۲ برای معنای دیگری دوباره استفاده نمی‌شود.

نتیجه و دلیل: تغییر اسم ظاهری با تغییر هویت wire فرق دارد؛ reuse شماره می‌تواند دادهٔ قدیمی را با معنای اشتباه بخواند.

Protocol Buffers (protobuf) is Google’s binary encoding library; Apache Thrift (from Facebook) is very similar. Every datum needs a schema — in the IDL language:

syntax = "proto3";

message Person {
  string user_name = 1;
  int64 favorite_number = 2;
  repeated string interests = 3;
}
  • ابزارِ codegen از این schema در زبان‌های مختلف class می‌سازد؛ کدِ برنامه به همان کدِ تولیدشده صدا می‌زند. زبانِ schema بسیار ساده‌تر از JSON Schema است: فقط فیلد و نوع، نه قواعدِ اعتبارسنجیِ پیچیده.
  • کدگذاریِ همان رکوردِ مثال فقط ۳۳ بایت است. برخلاف MessagePack، نامِ فیلدها در داده نیست؛ به‌جایش شمارهٔ تگ (tag) می‌آید: همان ۱ و ۲ و ۳ که در schema دیدی — یک نام‌مستعارِ فشرده برای فیلد.
  • نوعِ فیلد و تگ در یک بایت جا می‌شوند؛ اعداد طول‌متغیر (varint)‌اند: ۱۳۷۱ در دو بایت (بیتِ بالاییِ هر بایت یعنی «هنوز ادامه دارد»). اعدادِ −64..63 در یک بایت و −8192..8191 در دو بایت جا می‌شوند.
  • لیستِ صریح ندارد؛ repeated می‌گوید فیلد چند مقداری است و در بایت‌ها، تکرارِ همان تگ نمایش‌دهندهٔ آیتم‌هاست.
  • The codegen tool builds classes in many languages from this schema; application code calls into that generated code. The schema language is far simpler than JSON Schema: just fields and types, no complex validation rules.
  • Encoding the same example record takes only 33 bytes. Unlike MessagePack, field names aren’t in the data; instead come tag numbers: the 1, 2, 3 you saw in the schema — a compact alias for each field.
  • A field’s type and tag fit in one byte; numbers are variable-length (varint): 1337 fits in two bytes (each byte’s top bit means “more follows”). Numbers −64..63 fit in one byte, −8192..8191 in two.
  • There is no explicit list; repeated marks a multi-valued field, and in the bytes, repeating the same tag represents the items.
قواعد تکامل schema در protobufSchema-evolution rules in protobuf
  • رکوردِ کدشده فقط الحاقِ فیلدهای کدشده است؛ فیلدِ تنظیم‌نشده اصلاً نوشته نمی‌شود. پس تگ‌ها معنای داده را می‌سازند.
  • تغییر نامِ فیلد آزاد است (نام در داده نیست)؛ تغییر تگ ممنوع — همهٔ داده‌های موجود نامعتبر می‌شوند.
  • افزودنِ فیلدِ جدید با تگِ تازه = forward compatible: کدِ قدیمی تگِ ناشناخته را نادیده می‌گیرد و (اگر درست پیاده شود) بایت‌هایش را حفظ می‌کند. backward هم برقرار است چون داده‌های قدیمی برای فیلدِ تازه، مقدارِ پیش‌فرض می‌گیرند.
  • حذفِ فیلد آینهٔ افزودن است: تگِ قدیمی هرگز نباید دوباره استفاده شود (داده‌هایی هنوز با آن تگ نوشته شده‌اند)؛ می‌توان تگ‌های مستعمل را در schema reserve کرد.
  • تغییر نوعِ فیلد برای بعضی نوع‌ها ممکن است ولی خطر truncation دارد: ۳۲بیتی ← ۶۴بیتی برای کدِ جدید ساده است، اما کدِ قدیمی مقدار ۶۴بیتی را در متغیر ۳۲بیتی می‌ریزد و بریده می‌شود.
  • An encoded record is just concatenated encoded fields; an unset field is never written. So tags make the data’s meaning.
  • Renaming a field is free (names aren’t in the data); changing a tag is forbidden — all existing data becomes invalid.
  • Adding a field with a fresh tag = forward compatible: old code skips unknown tags and (if implemented right) preserves their bytes. Backward holds too, since old data takes the default for the fresh field.
  • Removing a field mirrors adding: the old tag must never be reused (data is still written with it); retired tags can be reserved in the schema.
  • Changing a field’s type is possible for some types but risks truncation: 32-bit → 64-bit is easy for new code, but old code pours a 64-bit value into a 32-bit variable and clips it.

۳.۵ Avro + نمودار ۳3.5 Avro + Diagram 3

در Avro، ساختارِ نویسنده و خواننده برای تفسیر داده کنار هم قرار می‌گیرند. نام فیلدها و قواعد سازگاری و مقدار پیش‌فرض، در تغییر ساختار اهمیت دارند.

مثال فنی: Resolution در Avro

وضعیت اولیه: writer فقط name دارد؛ reader تازه فیلد country با default برابر IR اضافه کرده است.

اجرای مرحله‌به‌مرحله: هنگام خواندن رکورد قدیمی، reader مقدارِ غایب country را از default می‌گیرد.

نتیجه و دلیل: default برای حلِ نبود فیلد در دادهٔ نویسنده است؛ جای الزام نوشتن فیلد در schema نویسنده را نمی‌گیرد.

Apache Avro (since 2009, a Hadoop subproject) differs from protobuf in interesting ways. It has two schema languages: Avro IDL for humans and a JSON rendering for machines. Example:

record Person {
  string userName;
  union { null, long } favoriteNumber = null;
  array<string> interests;
}
  • هیچ شمارهٔ تگی در کار نیست. کدگذاریِ همان رکورد فقط ۳۲ بایت است — فشرده‌ترینِ همه. در بایت‌ها نه نامِ فیلد هست و نه نوعِ داده؛ فقط مقادیرِ به‌هم‌چسبیده (رشته = طول + بایت‌های UTF-8). رمزگشاییِ درست فقط با دقیقاً همان schemaای که نوشتن با آن انجام شده ممکن است.
  • پس هنگام خواندن، برنامه با دو schema کار می‌کند: writer's schema (همانِ موقعِ نوشتن) و reader's schema (آنچه کدِ فعلی انتظار دارد). Avro با مقایسهٔ این دو، داده را از نمایِ writer به نمایِ reader ترجمه می‌کند.
  • No tag numbers at all. Encoding the same record takes only 32 bytes — the most compact of all. The bytes hold neither field names nor data types; just concatenated values (a string = length + UTF-8 bytes). Correct decoding is possible only with exactly the schema used at write time.
  • So at read time the program works with two schemas: the writer’s schema (the one at write time) and the reader’s schema (what current code expects). Avro translates the data from the writer’s view to the reader’s view by comparing the two.

قواعد resolution (فرمولِ شکل ۵-۶ کتاب)Resolution rules (the book’s Figure 5-6 formula)

خواننده باید دادهٔ نوشته‌شده با اسکیمای قبلی را طبق قواعدِ سازگاری به اسکیمای خودش نگاشت کند؛ مقدار پیش‌فرض برای فیلدِ غایب کاربرد دارد.

مثال فنی: فیلد اضافی و فیلد غایب

وضعیت اولیه: writer فیلدهای name, age دارد؛ reader فقط name, country می‌خواهد.

اجرای مرحله‌به‌مرحله: age موردنیاز reader نیست و کنار گذاشته می‌شود؛ country باید default معتبر داشته باشد، وگرنه resolution شکست می‌خورد.

نتیجه و دلیل: «نسخه‌ها فرق دارند» همیشه قابل‌حل نیست؛ قواعد تطبیق schema تعیین می‌کند خواندن مجاز است یا نه.

  • فیلدها بر اساس نام جفت می‌شوند؛ ترتیبِ فیلدها مهم نیست.
  • فیلدِ موجود در writer ولی نه در reader ← نادیده گرفته می‌شود.
  • فیلدِ موردِ انتظارِ reader که writer ننوشته ← با مقدارِ پیش‌فرضِ reader پر می‌شود.
  • برای سازگاری فقط می‌توان فیلدِ دارای مقدارِ پیش‌فرض را افزود یا حذف کرد. افزودنِ فیلدِ بدونِ default ← backward می‌شکند؛ حذفِ فیلدِ بدونِ default ← forward.
  • در Avro، null پیش‌فرضِ همه‌چیز نیست: برای nullable بودن باید union { null, ... } بنویسی و فقط وقتی می‌توانی default برابرِ null بگذاری که null شاخهٔ اول union باشد.
  • تغییر نامِ فیلد با alias در reader's schema ممکن است → backward هست ولی forward نیست. افزودنِ شاخه به union هم فقط backward است.
  • Fields pair by name; field order doesn’t matter.
  • A field present in the writer but not the reader is ignored.
  • A field the reader expects but the writer never wrote is filled with the reader’s default.
  • For compatibility you may only add or remove a field with a default. Adding a default-less field breaks backward; removing one breaks forward.
  • In Avro, null isn’t everything’s default: for nullability you must write union { null, ... }, and you may default to null only when null is the union’s first branch.
  • Renaming a field is possible with an alias in the reader’s schema → backward yes, but not forward. Adding a union branch is backward-only too.

اما writer's schema را از کجا بیاوریم؟But where do we get the writer’s schema?

برای تفسیرِ درستِ داده باید اسکیمای نویسنده در دسترس باشد؛ ممکن است همراه فایل یا با شناسه‌ای در مخزنِ اسکیما معرفی شود.

مثال فنی: پیدا کردن schema نویسنده

وضعیت اولیه: پیام Avro شناسهٔ schema برابر 23 دارد و بایت‌های رکورد را حمل می‌کند.

اجرای مرحله‌به‌مرحله: مصرف‌کننده schema ۲۳ را از registryِ مورد توافق می‌گیرد و همراه reader schema به decoder می‌دهد.

نتیجه و دلیل: داشتن فقط reader schema برای تفسیر همهٔ بایت‌ها کافی نیست؛ شناسه و دسترس‌پذیری schemaهای قدیمی باید حفظ شود.

زمینهراه‌حلنمونه
فایلِ بزرگِ پر از رکوردschema یک‌بار در ابتدای فایل نوشته می‌شود (object container files)dumpهای داده، فایل‌های Avro
پایگاه داده با رکوردهای تک‌تکشمارهٔ نسخه در ابتدای هر رکورد + فهرستِ schemaها در پایگاه دادهConfluent Schema Registry برای Kafka، LinkedIn Espresso
ارتباطِ شبکه‌ایمذاکرهٔ نسخهٔ schema هنگام باز شدنِ connection و استفاده تا پایانِ عمر آنپروتکلِ Avro RPC
ContextFixExample
A big file full of recordsThe schema is written once at the file’s start (object container files)Data dumps, Avro files
A database of individual recordsA version number at each record’s start + a schema list in the databaseConfluent Schema Registry for Kafka, LinkedIn Espresso
Network communicationSchema-version negotiation when the connection opens, used for its whole lifeThe Avro RPC protocol
نگه‌داشتنِ یک «پایگاه دادهِ نسخه‌های schema» به هر حال مفید است: هم سند (documentation) است و هم می‌توان سازگاریِ هر تغییر را قبل از deploy بررسی کرد. شمارهٔ نسخه می‌تواند عددِ افزایشی یا hash خودِ schema باشد.

Keeping a “schema-versions database” is useful anyway: it is documentation, and every change’s compatibility can be checked before deploy. Version numbers can be increasing integers or the schema’s own hash.

نمودار ۳ + روایت مدرسDiagram 3 + teacher narration

نمودار ۳ · Dataflow — حل اختلاف اسکیما با writer's و reader's schemaDiagram 3 · Dataflow — resolving schemas with writer’s and reader’s schemas باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۳Teacher narration

مثال فنی برای خواندنِ نمودار: Resolution در Avro

وضعیت اولیه: writer فقط name دارد؛ reader تازه فیلد country با default برابر IR اضافه کرده است.

اجرای مرحله‌به‌مرحله: هنگام خواندن رکورد قدیمی، reader مقدارِ غایب country را از default می‌گیرد.

نتیجه و دلیل: default برای حلِ نبود فیلد در دادهٔ نویسنده است؛ جای الزام نوشتن فیلد در schema نویسنده را نمی‌گیرد.

چهار ستون داریم که عنوان‌شان از چپ به راست است: «Write Side»، «Encoded Bytes»، «Read Side» و «Resolution». نوارِ بالا جریانِ اصلی و نوارِ پایین، «پشتیبانِ schema» است.

Four columns, left to right: Write Side, Encoded Bytes, Read Side, Resolution. The upper lane is the main flow; the lower lane is schema backstage.

در نوارِ بالا: «Writer App» (زیرنویس: «compiled with schema»، برچسبِ «writer's schema») با فلشِ پررنگِ «encode values in schema order» به «Encoded Record» می‌رود (زیرنویس: «values only, no names»، برچسبِ «32 bytes») — دقت کنید هیچ نامی در بایت‌ها نیست. از آن فلشِ پررنگِ «bytes + version number» به «Reader App» (زیرنویس: «uses two schemas») می‌رسد.

Top lane: Writer App (compiled with schema; tag writer’s schema) goes via bold encode values in schema order to the Encoded Record (values only, no names; 32 bytes) — note no names in the bytes. From there bold bytes + version number reaches the Reader App (uses two schemas).

نوارِ پایین سه جعبهٔ حمایتی دارد. از «Writer's Schema» (زیرنویس: «fields in write order»، برچسبِ «defaults declared») دو فلشِ چین‌چین می‌آید: یکی بالا به Writer App با برچسبِ «writer's schema» (قراردادِ compile-time) و دیگری به راست به «Schema Registry» با برچسبِ «register schema version». از Registry هم فلشِ چین‌چینِ «fetch writer's schema» بالا می‌رود و به Reader App می‌رسد — همان «شمارهٔ نسخه ← schema» که در جدولِ بالا دیدید.

The lower lane holds three support boxes. From Writer’s Schema (fields in write order; defaults declared) come two dashed arrows: one up to Writer App tagged writer’s schema (the compile-time contract), one right to the Schema Registry tagged register schema version. From the Registry the dashed fetch writer’s schema climbs to Reader App — the “version number → schema” trick from the table above.

سومین جعبهٔ پایینی «Reader's Schema» (زیرنویس: «fields code expects»، برچسبِ «may differ») است که با فلشِ چین‌چینِ «reader's schema» به Reader App می‌رود. بعد Reader App با فلشِ پررنگِ «two schemas to resolve» به «Schema Resolution» (زیرنویس: «match by field name»، برچسبِ «Avro spec rules») و از آن فلشِ «translate to reader's schema» به «Decoded Record» (زیرنویس: «defaults filled in»، برچسبِ «extras ignored»).

The third lower box, Reader’s Schema (fields code expects; may differ), goes via dashed reader’s schema to Reader App. Then Reader App takes bold two schemas to resolve to Schema Resolution (match by field name; Avro spec rules), and translate to reader’s schema on to the Decoded Record (defaults filled in; extras ignored).

درسِ شکل: خواندنِ داده در Avro یعنی ترجمه بین دو schema، نه فقط رمزگشاییِ بایت‌ها. writer's schema را یا خودِ فایل دارد، یا registry با شمارهٔ نسخه می‌دهد؛ reader's schema همان چیزی است که کدِ امروزِ تو انتظار دارد. و دو قاعدهٔ طلایی resolution در برچسبِ آخر خلاصه شده: extras ignored و defaults filled in.

The lesson: reading data in Avro means translating between two schemas, not just decoding bytes. The writer’s schema comes either inside the file itself or from a registry via the version number; the reader’s schema is whatever today’s code expects. And the two golden resolution rules sit in the last tag: extras ignored and defaults filled in.

۳.۶ مزایای schema و اسکیماهای پویا3.6 Schema benefits and dynamic schemas

اسکیما قراردادِ شکل و معنای داده را روشن می‌کند. داشتن فایل اسکیما کافی نیست؛ باید نسخه‌ها، قواعد تغییر و دسترسیِ خواننده به ساختار مناسب هم مدیریت شوند.

مثال فنی: قرارداد پیش از ورود داده

وضعیت اولیه: پیام ورودی فیلد age را با متن old می‌فرستد، اما قرارداد عدد می‌خواهد.

اجرای مرحله‌به‌مرحله: validation پیام را رد یا قرنطینه می‌کند و مسیرِ خطا را ثبت می‌کند؛ صرف تولید کلاس از schema کافی نیست.

نتیجه و دلیل: schema اختلاف نوع را زود آشکار می‌کند؛ درستی قواعد دامنه مثل سن منفی هنوز کنترل جدا می‌خواهد.

  • اسکیماهای پویا با Avro راحت‌ترند: چون تگی در schema نیست، می‌توان مستقیم از schema رابطه‌ای، Avro schema تولید کرد (هر جدول ← یک record، هر ستون ← یک فیلد) و dump کاملِ پایگاه داده را نوشت. اگر schema پایگاه داده عوض شود، همان مبدل دوباره اجرا می‌شود؛ چون فیلدها با نام شناخته می‌شوند، reader's schema قدیمی هنوز جفت می‌شود. در protobuf باید تگ‌ها را دستی تخصیص دهی و مراقب باشی تگی تکراری استفاده نشود.
  • قالب‌های باینریِ مبتنی بر schema چند مزیتِ بزرگ دارند: فشرده‌تر از «JSONهای باینری»‌اند (نامِ فیلدها حذف می‌شود)؛ خودِ schema سندِ زنده است (برخلاف مستنداتِ دستی، همیشه به‌روز است چون برای decode لازم است)؛ با نگه‌داشتنِ registry می‌توان سازگاریِ تغییرات را قبل از deploy سنجید؛ و برای زبان‌های statically typed کد تولید می‌کند (بررسیِ نوع در compile-time).
  • این ایده‌ها تازه نیستند: ASN.1 از ۱۹۸۴ با تگ‌ها تکاملِ schema داشت (هنوز در گواهی‌های SSL/X.509 با کدگذاریِ DER استفاده می‌شود) اما پیچیده و بدمستند است. بیشتر پایگاه‌های داده رابطه‌ای هم پروتکلِ باینریِ اختصاصی خودشان را دارند که driver (JDBC/ODBC) آن را decode می‌کند.
  • قاعدهٔ عملی: تعدادِ قالب‌های هم‌زمان در سیستم را کم نگه دار؛ هر قالبِ اضافه یعنی عملیاتِ بیشتر برای نگه‌داشتنِ سازگاری.
  • Dynamic schemas are easier with Avro: with no tags in the schema you can generate an Avro schema straight from a relational schema (each table → one record, each column → one field) and write a full database dump. If the database schema changes, re-run the same converter; since fields are known by name, the old reader’s schema still pairs up. In protobuf you must allocate tags by hand and watch none get reused.
  • Schema-driven binary formats carry big wins: denser than “JSON binaries” (field names drop out); the schema itself is living documentation (unlike hand docs, always current since decoding needs it); with a registry you can test a change’s compatibility before deploy; and they generate code for statically typed languages (compile-time type checks).
  • These ideas aren’t new: ASN.1 has had tag-based schema evolution since 1984 (still used in SSL/X.509 certificates with DER encoding) but is complex and badly documented. Most relational databases also have their own proprietary binary protocol that the driver (JDBC/ODBC) decodes.
  • Practical rule: keep the number of simultaneous formats in the system small; each extra format means more work keeping compatibility.
محورJSON / XMLMessagePack و دوستانProtocol BuffersAvro
schemaاختیاری (JSON Schema)ندارداجباری (IDL با تگ)اجباری (IDL/JSON بدون تگ)
شناسهٔ فیلد در دادهنامِ کاملنامِ کاملشمارهٔ تگهیچ (فقط ترتیبِ schema)
حجمِ رکوردِ مثال۸۱ بایت۶۶ بایت۳۳ بایت۳۲ بایت
سازگاریبستگی به قراردادِ شما داردمثل JSONبا تگ‌های ثابت: روشن و قابل‌اتکابا defaultها و resolution: روشن و قابل‌اتکا
انسان‌خوانبلهنهنه (ابزار می‌خواهد)نه (ابزار می‌خواهد)
بهترین کاربردتبادلِ بین سازمان‌ها، APIهای عمومیوقتی فقط می‌خواهی JSON کوچک‌تر شودRPC (gRPC)، لاگ‌ها، پیام‌هافایل‌های بزرگ، dump، pipelineهای داده

نمودار مفهومی — یک رکورد، چهار کدگذاریConcept diagram — one record, four encodings

نمودار مفهومی · چرا schema بایت‌ها را آب می‌کندConcept diagram · Why schemas shrink bytes باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: مقایسهٔ قالب روی همان رکورد

وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب می‌نویسیم.

اجرای مرحله‌به‌مرحله: JSON نام فیلدها را متنی حمل می‌کند؛ قالب مبتنی بر schema می‌تواند شناسه یا جایگاه فیلد را به‌کار گیرد.

نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن به‌تنهایی تضمین کمترین حجم یا بهترین تکامل نیست.

چهار میله را از بالا بخوانید: JSON با ۸۱ بایت (نام فیلدها داخل داده سفر می‌کنند)، MessagePack با ۶۶ بایت (همان مدل، باینری، ولی هنوز بی‌schema)، پروتوباف با ۳۳ بایت (نام‌ها با شمارهٔ تگ جایگزین شده‌اند) و Avro با ۳۲ بایت (فقط مقادیر؛ ترتیب از schema می‌آید). جملهٔ پایین درس است: schema نام‌ها را حذف می‌کند و ۸۱ بایت را ۳۲ می‌کند.

Read the four bars top-down: JSON at 81 bytes (field names travel inside the data), MessagePack at 66 (same model, binary, still schema-less), Protobuf at 33 (names replaced by tag numbers) and Avro at 32 (values only; order comes from the schema). The bottom line is the lesson: the schema drops the names, turning 81 bytes into 32.

AspectJSON / XMLMessagePack & friendsProtocol BuffersAvro
SchemaOptional (JSON Schema)NoneMandatory (tagged IDL)Mandatory (tagless IDL/JSON)
Field ID in dataFull nameFull nameTag numberNone (schema order only)
Example-record size81 bytes66 bytes33 bytes32 bytes
CompatibilityDepends on your conventionsLike JSONWith stable tags: clear and reliableWith defaults + resolution: clear and reliable
Human-readableYesNoNo (needs tools)No (needs tools)
Best useCross-org exchange, public APIsWhen you just want smaller JSONRPC (gRPC), logs, messagesBig files, dumps, data pipelines

۳.۷ چرخهٔ حیاتِ یک فیلدِ اسکیما + نمودار ۶3.7 A schema field’s lifecycle + Diagram 6

تغییر یک فیلد بهتر است مرحله‌ای انجام شود: خواننده‌ها را آماده کنیم، نوشتن شکل تازه را شروع کنیم و پس از کنار رفتن وابستگی‌های قدیمی، شکل قبلی را حذف کنیم.

مثال فنی: چرخهٔ حذف یک فیلد

وضعیت اولیه: سرویس از full_name به given_name, family_name مهاجرت می‌کند.

اجرای مرحله‌به‌مرحله: ابتدا خواننده‌ها هر دو شکل را تحمل می‌کنند؛ writerها مرحله‌ای عوض می‌شوند؛ حذف نهایی وقتی است که مصرف‌کننده و دادهٔ قدیمی پوشش داده شده‌اند.

نتیجه و دلیل: حذف فیلد فقط تغییر یک فایل schema نیست؛ عمر داده و همهٔ readerها بخشی از مهاجرت‌اند.

So far we’ve seen how writer’s and reader’s schemas get along via resolution rules: the writer’s extra field is ignored, the writer’s missing field is filled with the reader’s default. But these rules only save us if schema change arrives step by step: in a live system, during a rolling upgrade old and new code versions run side by side for hours or days, and every message must stay readable to both. So “adding one field” isn’t one deploy; it’s a multi-stage cycle, each step guaranteeing one compatibility.

قانونِ طلاییِ این چرخه readers first است: اول کدی را deploy کنید که فیلدِ تازه را می‌خواند و اگر نبود تحملش می‌کند، بعد کدی را که آن را می‌نویسد. ارزان‌ترینِ تغییرها، افزودنِ فیلدِ اختیاری با مقدارِ پیش‌فرض است — در Avro یعنی union { null, ... } با default مناسب — چون خواننده‌های قدیمی نادیده‌اش می‌گیرند و خواننده‌های جدید با پیش‌فرضِ خودشان پرش می‌کنند. اما تغییرِ نام یا نوعِ یک فیلد، در حکمِ «حذف + افزودن» است: اگر در یک گام انجام شود، یکی از دو سازگاریِ backward/forward می‌شکند.

This cycle’s golden rule is readers first: first deploy code that reads the fresh field and tolerates its absence, then code that writes it. The cheapest changes add an optional field with a default — in Avro a union { null, ... } with a proper default — since old readers ignore it and new readers fill it with their own default. But renaming or retyping a field equals “delete + add”: done in one step, one of the two backward/forward compatibilities breaks.

و چرا حذف همیشه آخرین گام است؟ چون داده از کد عمرش بیشتر است: پیام‌های قدیمیِ مانده در صف‌ها، backupها و فایل‌های آرشیوی برای همیشه با همان اسکیمای قدیمی می‌مانند. پس اول نوشتنِ فیلد را متوقف می‌کنیم (deprecated) اما خواننده‌ها هنوز باید رکوردهای حاویِ آن را بخوانند، و فقط وقتی هیچ خواننده‌ای به آن نیاز نداشت، از اسکیما حذفش می‌کنیم. نمودار ۶ همین چرخه را به‌صورتِ یک ماشینِ وضعیت می‌کشد: پنج حالتِ اصلیِ فیلد، دو «دروازهٔ سازگاری» برای تصمیم‌های سخت، و یک وضعیتِ بن‌بست برای وقتی که ترتیب را اشتباه رفته‌ایم.

And why is deletion always the last step? Because data outlives code: old messages stuck in queues, backups and archive files keep their old schema forever. So first stop writing the field (deprecated) while readers must still read records containing it, and only drop it from the schema once no reader needs it. Diagram 6 draws this cycle as a state machine: five main field states, two compatibility gates for the hard calls, and one dead-end state for wrong ordering.

نمودار ۶ · Lifecycle — چرخهٔ حیاتِ فیلد در تکاملِ schemaDiagram 6 · Lifecycle — a field’s life in schema evolution باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۶Teacher narration

مثال فنی برای خواندنِ نمودار: چرخهٔ حذف یک فیلد

وضعیت اولیه: سرویس از full_name به given_name, family_name مهاجرت می‌کند.

اجرای مرحله‌به‌مرحله: ابتدا خواننده‌ها هر دو شکل را تحمل می‌کنند؛ writerها مرحله‌ای عوض می‌شوند؛ حذف نهایی وقتی است که مصرف‌کننده و دادهٔ قدیمی پوشش داده شده‌اند.

نتیجه و دلیل: حذف فیلد فقط تغییر یک فایل schema نیست؛ عمر داده و همهٔ readerها بخشی از مهاجرت‌اند.

این نمودار سه نوارِ (lane) افقی دارد که عنوانشان از بالا به پایین این است: «Field states» (زندگیِ یک فیلد)، «Compatibility gates» (دروازه‌های تصمیمِ سازگاری) و «Terminal» (وضعیتِ بن‌بست). پنج حالتِ نوارِ بالا شمارهٔ گام دارند — «01» تا «05» — و مسیرِ عادیِ زندگیِ یک فیلد را می‌سازند؛ دو حالتِ لوزی‌شکلِ نوارِ میانی «دروازه»‌اند و حالتِ قرمزِ نوارِ پایین، مقصدِ مسیرِ شکست.

The diagram has three horizontal lanes, top to bottom: Field states (a field’s life), Compatibility gates (compatibility decision gates), Terminal (dead end). The top lane’s five states are numbered 01–05 and form a field’s normal life; the middle lane’s two diamonds are gates, the bottom lane’s red state the failure destination.

از چپِ نوارِ بالا شروع کنید: «Proposed» (گامِ «01») با زیرنویسِ «schema change drafted» و برچسبِ «not deployed» — یعنی تغییر هنوز فقط روی کاغذ است و به هیچ سروری نرسیده. فلشِ پررنگِ «deploy readers first» آن را به «Added Optional» (گامِ «02») می‌برد — زیرنویسِ «new field, default or null» و برچسبِ «writers may omit it»: فیلد در اسکیما هست و خواننده‌ها تحملش می‌کنند، اما هنوز هیچ نویسنده‌ای پرش نمی‌کند. فلشِ بعدی، «writers fill it»، به «In Use» (گامِ «03») می‌رسد — زیرنویسِ «new writers fill it» و برچسبِ «old readers ignore it» — تنها حالتی از این پنج‌تا که فیلد واقعاً داده حمل می‌کند.

Start left on the top lane: Proposed (step 01; schema change drafted; not deployed) — the change is still only on paper. The bold deploy readers first leads to Added Optional (step 02; new field, default or null; writers may omit it): the field is in the schema and readers tolerate it, but no writer fills it yet. Next, writers fill it reaches In Use (step 03; new writers fill it; old readers ignore it) — the only one of the five where the field really carries data.

از اینجا داستانِ جمع‌وجور کردن است. فلشِ «stop writing it» به «Deprecated» (گامِ «04») می‌رود — زیرنویسِ «no new writes» و برچسبِ «readers still tolerate it»: نوشتنِ تازه متوقف شده، اما رکوردهای قدیمی هنوز فیلد را دارند و خواننده‌ها باید بتوانند همان‌ها را بخوانند. سرانجام فلشِ «drop the field» به «Removed» (گامِ «05») می‌رسد — زیرنویسِ «gone from the schema» و برچسبِ اطمینان‌بخشِ «safe to drop». دقت کنید این پنج گام فقط به همین ترتیب امن‌اند؛ برای هر جابه‌جاییِ ترتیب، یکی از دو دروازهٔ نوارِ میانی باید جوابِ «بله» بدهد.

From here it’s the winding-down story. stop writing it goes to Deprecated (step 04; no new writes; readers still tolerate it): fresh writes stop, but old records still carry the field and readers must keep reading them. Finally drop the field reaches Removed (step 05; gone from the schema; reassuringly safe to drop). These five steps are only safe in this order; any reordering must pass one of the middle lane’s two gates.

حالا دروازه‌ها. از «Added Optional» فلشِ چین‌چینِ «need it now?» به پایین می‌آید و به نوارِ «Compatibility gates» می‌رسد: حالتِ لوزیِ «All Writers Upgraded?» با زیرنویسِ «old code still running» و برچسبِ «gate». اگر همهٔ نویسنده‌ها بالا رفته باشند، فلشِ «yes, all upgraded» مستقیم به «In Use» برمی‌گردد — یعنی نوشتنِ فیلد فقط بعد از عبور از این دروازه شروع می‌شود. اما اگر نسخه‌ای قدیمی هنوز در حال اجرا باشد، فلشِ قرمزِ «no, mixed fleet» به نوارِ «Terminal» می‌رود: «Breaking Change» با زیرنویسِ «old code crashes or drops data» و برچسبِ «roll back» — یعنی یا کدِ قدیمی می‌شکند یا داده را می‌بلعد، و تنها راه، برگرداندنِ deploy است.

Now the gates. From Added Optional the dashed need it now? descends to the Compatibility gates lane: the diamond All Writers Upgraded? (old code still running; gate). If all writers are up, yes, all upgraded returns straight to In Use — writing the field starts only past this gate. But if an old version still runs, the red no, mixed fleet drops to the Terminal lane: Breaking Change (old code crashes or drops data; roll back) — either old code breaks or it swallows data, and the only way out is rolling the deploy back.

دروازهٔ دوم سمتِ راستِ همان نوار است: از «Deprecated» فلشِ چین‌چینِ «can we drop it?» پایین می‌آید به «All Readers Upgraded?» با زیرنویسِ «ignore unknown fields» و برچسبِ «gate». جوابِ مثبت — فلشِ «yes, all upgraded» — فیلد را به «Removed» می‌رساند؛ اما جوابِ منفی — فلشِ «not yet» — درست به خودِ «Deprecated» برمی‌گردد: حلقه‌ای که ممکن است هفته‌ها و ماه‌ها تکرار شود تا آخرین خواننده‌ای که هنوز به فیلد نیاز دارد بازنشسته شود. حذفِ فیلد فقط وقتی «safe to drop» است که این دروازه برای همیشه بسته شده باشد.

The second gate sits right on the same lane: from Deprecated the dashed can we drop it? descends to All Readers Upgraded? (ignore unknown fields; gate). A yes — yes, all upgraded — carries the field to Removed; a no — not yet — loops right back to Deprecated: a loop that may repeat for weeks and months until the last reader still needing the field retires. Dropping is safe to drop only once this gate closes for good.

درسِ اولِ شکل را در یک ترتیب جمع کنید: readers first، writers second، drop last. اول کدِ خواننده را بالا ببرید تا فیلدِ ناشناخته را تحمل کند، بعد کدِ نویسنده را تا فیلد را پر کند، و حذف را فقط آخرِ کار و پس از هر دو دروازه انجام دهید. در یک rolling upgrade واقعی یعنی deployها را باید با همین ترتیب برنامه‌ریزی کنید: نسخه‌ای که فقط «می‌خواند» همیشه ایمن است، نسخه‌ای که «می‌نویسد» فقط بعد از آن.

Sum the figure’s first lesson into one order: readers first, writers second, drop last. First raise reader code to tolerate the unknown field, then writer code to fill it, and delete only at the very end, past both gates. In a real rolling upgrade that means scheduling deploys in exactly this order: a version that only “reads” is always safe; one that “writes” only after it.

و درسِ دوم: داده از کد عمرش بیشتر است، و هر تغییرِ «ظاهراً کوچک» ممکن است در لباسِ «حذف + افزودن» باشد — تغییرِ نامِ فیلد، تغییرِ نوع، یا حتی عوض کردنِ مقدارِ پیش‌فرض. قیمتِ به‌هم‌ریختنِ ترتیب همان یک فلشِ قرمزِ «no, mixed fleet» است: «Breaking Change» و بعد «roll back». اگر هر تغییرِ اسکیما را با همین پنج گام و دو دروازه بسنجید، deployِ بدونِ downtime دیگر شانس نیست؛ نتیجهٔ منطقیِ ترتیبِ درست است.

And the second lesson: data outlives code, and every “seemingly small” change may wear a “delete + add” costume — renaming a field, retyping it, even changing its default. The price of scrambling the order is that one red arrow no, mixed fleet: Breaking Change, then roll back. Weigh every schema change against these five steps and two gates, and downtime-free deploy stops being luck — it becomes the logical result of the right order.

۴4حالت‌های جریان دادهDataflow modes

داده می‌تواند از راه ذخیره‌سازی، درخواست مستقیم، صف پیام یا گردش‌کار میان برنامه‌ها منتقل شود. در هر مسیر باید بدانیم چه کسی تولید می‌کند، چه کسی می‌خواند و چه مدت بعد.

مثال فنی: سه مسیر همان داده

وضعیت اولیه: تغییر نام کاربر می‌تواند از رکورد پایگاه داده، پاسخ API یا پیام رویداد خوانده شود.

اجرای مرحله‌به‌مرحله: پایگاه داده نسخه‌های قدیمی را نگه می‌دارد؛ API با کلاینت‌های قدیمی سروکار دارد؛ صف ممکن است پیام قدیمی را دوباره تحویل دهد.

نتیجه و دلیل: هر مسیر پنجرهٔ سازگاری خودش را دارد؛ پایان انتشار سرورها لزوماً پایان دادهٔ قدیمی نیست.

Compatibility is a relation between one process that encodes and another that decodes. But data travels between processes in assorted ways. This part looks at four common modes.

۴.۱ نمودار ۴ + روایت مدرس4.1 Diagram 4 + teacher narration

نمودار ۴ · Architecture — چهار حالت جریان داده و جای encode/decodeDiagram 4 · Architecture — four dataflow modes and where encode/decode sit باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۴Teacher narration

مثال فنی برای خواندنِ نمودار: سه مسیر همان داده

وضعیت اولیه: تغییر نام کاربر می‌تواند از رکورد پایگاه داده، پاسخ API یا پیام رویداد خوانده شود.

اجرای مرحله‌به‌مرحله: پایگاه داده نسخه‌های قدیمی را نگه می‌دارد؛ API با کلاینت‌های قدیمی سروکار دارد؛ صف ممکن است پیام قدیمی را دوباره تحویل دهد.

نتیجه و دلیل: هر مسیر پنجرهٔ سازگاری خودش را دارد؛ پایان انتشار سرورها لزوماً پایان دادهٔ قدیمی نیست.

شکل از چپ شروع می‌شود: جعبهٔ «Application Process» با زیرنویسِ «in-memory objects». فلشِ پررنگِ «encode object to bytes» آن را به قلبِ شکل می‌رساند: جعبهٔ بزرگِ «Encoder / Decoder» (زیرنویس: «objects to bytes and back»، برچسبِ «textual or schema-driven»).

The figure starts left: Application Process (in-memory objects). The bold encode object to bytes carries it to the figure’s heart: the big Encoder / Decoder box (objects to bytes and back; textual or schema-driven).

از راستِ این جعبه، چهار فلش به چهار حالتِ جریان داده می‌رود و همه داخل قابِ «Bytes cross a process boundary — backward + forward compatibility required» جمع شده‌اند:

From this box’s right, four arrows run to four dataflow modes, all gathered inside Bytes cross a process boundary — backward + forward compatibility required:

  • بالاترین: «Database» (زیرنویس: «writer encodes, later reader decodes»، برچسبِ «data outlives code») با فلشِ پررنگِ «persist and read records».
  • دوم (هم‌ترازِ Encoder): «Services: REST / RPC» (زیرنویس: «client encodes request, server replies»، برچسبِ «evolve APIs independently») با فلشِ «request and response payloads».
  • سوم: «Workflow Engine» (زیرنویس: «steps logged and replayed»، برچسبِ «durable execution») با فلشِ «step results to durable log».
  • پایینی: «Broker / Actors» (زیرنویس: «sender encodes, subscribers decode»، برچسبِ «async messages») با فلشِ چین‌چینِ «publish events and messages».
  • Top: Database (writer encodes, later reader decodes; data outlives code) via bold persist and read records.
  • Second (level with the Encoder): Services: REST / RPC (client encodes request, server replies; evolve APIs independently) via request and response payloads.
  • Third: Workflow Engine (steps logged and replayed; durable execution) via step results to durable log.
  • Bottom: Broker / Actors (sender encodes, subscribers decode; async messages) via dashed publish events and messages.
درسِ شکل: در هر چهار حالت، کارِ یکسان است — object به بایت و بایت به object — اما چه کسی encode می‌کند، چه کسی decode، و کِی فرق می‌کند. همین تفاوت‌ها تعیین می‌کند هر حالت چه سازگاری‌هایی لازم دارد؛ بقیهٔ این بخش تک‌تکِ همین چهار شاخه را باز می‌کند.

The lesson: in all four modes the job is identical — object to bytes and bytes to object — but who encodes, who decodes, and when differ. Those very differences decide what compatibilities each mode needs; the rest of this part unpacks these four branches one by one.

۴.۲ جریان داده از طریق پایگاه داده4.2 Dataflow via databases

نوشتن در پایگاه داده، نوعی تحویل داده به خوانندهٔ آینده است. ممکن است نسخهٔ بعدی برنامه رکوردهایی را بخواند که سال‌ها قبل ساخته شده‌اند.

مثال فنی: ویرایش نام، حذف email

وضعیت اولیه: نسخهٔ ۲ رکورد {name:Sara,email:a@b} نوشته است؛ نسخهٔ ۱ فقط name را می‌شناسد.

اجرای مرحله‌به‌مرحله: نسخهٔ ۱ کل شیء را می‌خواند، name را عوض می‌کند و بدون email بازمی‌نویسد؛ update محدود به name یا حفظ فیلد ناشناخته جلوی حذف را می‌گیرد.

نتیجه و دلیل: یک read-modify-write قدیمی می‌تواند دادهٔ جدید را پاک کند، حتی وقتی decode بدون خطا بوده است.

  • در پایگاه داده، processِ نویسنده encode می‌کند و processِ خواننده decode. اگر فقط یک process داشته باشی، خواننده همان برنامه در آینده است: ذخیره کردن یعنی پیام فرستادن به خودِ آینده‌ات — پس backward compatibility قطعاً لازم است.
  • اما معمولاً چند process هم‌زمان به پایگاه داده دسترسی دارند (سرویس‌های مختلف یا چند instance از یک سرویس) و در میانهٔ rolling upgrade بعضی‌ها جدید و بعضی‌ها قدیمی‌اند؛ پس forward compatibility هم لازم است.
  • data outlives code: ممکن است مقداری ۵ میلی‌ثانیه پیش و مقداری ۵ سال پیش نوشته شده باشد. کد در چند دقیقه عوض می‌شود؛ داده تا بازنویسیِ صریح با همان فرمتِ قدیمی می‌ماند.
  • بازنویسی (migration) داده به schema جدید ممکن است ولی روی دادهٔ بزرگ گران است؛ پس موتورها آن را به‌تعویق‌انداخته و best-effort انجام می‌دهند: موتورهای LSM هنگام compaction با جدیدترین فرمت بازنویسی می‌کنند؛ پایگاه‌های داده رابطه‌ای با افزودنِ ستونِ nullable بدونِ rewrite کار می‌کنند (هنگام خواندنِ ردیفِ قدیمی، null پر می‌شود). نتیجه: کلِ پایگاه داده «انگار» با یک schema کد شده، هرچند ذخیره‌سازی زیرین مخلوطی از نسخه‌هاست.
  • تغییراتِ پیچیده‌تر (تک‌مقداری ← چندمقداری، جابه‌جایی به جدولِ جدا) هنوز به بازنویسیِ سطحِ برنامه نیاز دارند و نگه‌داشتنِ سازگاری در چنین migrationهایی هنوز پژوهشی است.
  • بایگانی (archival): snapshot یا dump معمولاً با جدیدترین schema و به‌صورت یکدست نوشته می‌شود (چون اصل داده در حال کپی است). چون dump یک‌جا نوشته و تغییرناپذیر می‌شود، Avro object container files عالی‌اند و فرصتِ خوبی است برای کدگذاریِ ستونی مثل Parquet (فصل ۱۱).
  • In a database the writer process encodes and the reader process decodes. With a single process the reader is the same program in the future: storing means messaging your future self — so backward compatibility is definitely needed.
  • But usually several processes touch the database at once (different services, or several instances of one service), and mid-rolling-upgrade some are new and some old; so forward compatibility is needed too.
  • Data outlives code: one value may be 5 milliseconds old, another 5 years. Code changes in minutes; data keeps its old format until explicitly rewritten.
  • Rewriting (migrating) data to a new schema is possible but expensive on big data; so engines do it deferred and best-effort: LSM engines rewrite in the newest format during compaction; relational databases add nullable columns without rewrites (reading an old row fills null). Result: the whole database looks “as if” encoded with one schema, though the underlying storage mixes versions.
  • Trickier changes (single- → multi-valued, moving to a separate table) still need application-level rewrites, and keeping compatibility through such migrations is still research territory.
  • Archival: a snapshot or dump is usually written uniformly with the newest schema (since the actual data is being copied). Since a dump is written at once and frozen, Avro object container files are ideal — and a good moment for columnar encodings like Parquet (Chapter 11).

۴.۳ سرویس‌ها: REST و RPC4.3 Services: REST and RPC

  • الگوی رایج: کلاینت و سرور. سرور API را در شبکه می‌گذارد و کلاینت به آن درخواست می‌فرستد. وب همین‌طور کار می‌کند (GET برای گرفتن HTML/CSS/JS، POST برای فرستادن داده) با مجموعه‌ای استاندارد از پروتکل‌ها (HTTP، URL، TLS، HTML).
  • کلاینت فقط مرورگر نیست: اپ‌های موبایل و دسکتاپ و اپ‌های JavaScript مرورگر هم با HTTP به سرور وصل می‌شوند و معمولاً JSON می‌گیرند.
  • سرویس‌ها شبیه پایگاه داده‌اند (داده می‌پذیرند و می‌دهند) اما به‌جای زبانِ کوئریِ عمومی، API ویژهٔ برنامه دارند با ورودی/خروجی‌های ازپیش‌تعیین‌شده — همین، کپسوله‌سازی می‌آورد.
  • هدفِ معماری microservices این است که هر سرویس مستقل deploy و تکامل یابد (معمولاً یک تیم، یک سرویس، releaseهای مکرر). پس نسخه‌های قدیم و جدیدِ سرور و کلاینت هم‌زمان‌اند و فقط تا وقتی APIها سازگار بمانند، تیم‌ها آزادند زیرساخت را عوض کنند.
  • web service یعنی سرویسی که زیرینش HTTP است؛ در سه زمینه به کار می‌رود: کلاینتِ روی دستگاهِ کاربر (اینترنتِ عمومی)، سرویس‌به‌سرویسِ درونِ یک سازمان (شبکهٔ خصوصی)، و سازمان‌به‌سازمان (APIهای عمومی مثل پردازشِ پرداخت یا OAuth).
  • REST محبوب‌ترین فلسفهٔ طراحی است: فرمت‌های ساده، URL برای شناساییِ منابع، و امکاناتِ HTTP برای cache، احراز هویت و مذاکرهٔ نوعِ محتوا. اما کلاینت هنوز باید بداند کدام endpoint و چه فرمتی دارد؛ برای همین IDL می‌نویسند: OpenAPI (Swagger) برای سرویس‌های JSON و Protocol Buffers برای gRPC. ابزارهای IDL مستندات می‌سازند، سازگاریِ تغییرات schema را بررسی می‌کنند و GUI برای آزمایش می‌دهند.
  • The common pattern: client and server. The server puts an API on the network and the client sends it requests. The web works this way (GET for HTML/CSS/JS, POST for sending data) with a standard protocol set (HTTP, URL, TLS, HTML).
  • Clients aren’t just browsers: mobile and desktop apps and browser JavaScript apps also reach servers over HTTP, usually taking JSON.
  • Services resemble databases (they take and give data) but instead of a general query language they expose a program-specific API with predefined inputs/outputs — which brings encapsulation.
  • The microservices goal is for each service to deploy and evolve independently (usually one team, one service, frequent releases). So old and new server and client versions coexist, and teams are free to change internals only while APIs stay compatible.
  • A web service is a service over HTTP; used in three settings: user-device clients (the public internet), service-to-service inside one organization (private network), and org-to-org (public APIs like payment processing or OAuth).
  • REST is the most popular design philosophy: simple formats, URLs for resource identity, and HTTP facilities for caching, auth and content negotiation. But the client must still know which endpoint and format; hence IDLs: OpenAPI (Swagger) for JSON services, Protocol Buffers for gRPC. IDL tools build docs, check schema-change compatibility, and give testing GUIs.
چرا «فراخوانی از راه دور» مثل فراخوانیِ محلی نیست؟Why isn’t a “remote call” like a local call?

در REST یا RPC، برنامه از سرویس دیگری کاری می‌خواهد. این فراخوانی روی شبکه است و ممکن است دیر برسد، پاسخ آن گم شود یا نتیجه‌اش نامشخص بماند.

مثال فنی: RPC ناموفق یا پاسخ نامعلوم؟

وضعیت اولیه: کلاینت CreateOrder(request_id=r7) را می‌فرستد؛ سرور سفارش را ثبت می‌کند، ولی پاسخ نمی‌رسد.

اجرای مرحله‌به‌مرحله: retry با همان شناسه به همان نتیجه وصل می‌شود؛ retry با شناسهٔ تازه می‌تواند سفارش دوم بسازد.

نتیجه و دلیل: برخلاف فراخوانی محلی، timeout شبکه نتیجهٔ عملیات دوردست را مشخص نمی‌کند؛ API باید این ابهام را مدیریت کند.

The RPC model wants a network request to look like calling a function (location transparency) — but the differences are fundamental:

  • فراخوانیِ محلی قابل‌پیش‌بینی است؛ درخواستِ شبکه‌ای غیرقابل‌پیش‌بینی (گم‌شدن، کندیِ ماشینِ دور).
  • نتیجهٔ فراخوانیِ محلی یا مقدار است یا exception؛ درخواستِ شبکه‌ای یک پایانِ سوم هم دارد: timeout بدون نتیجه — نمی‌دانی اصلاً اجرا شد یا نه.
  • تلاشِ مجدد ممکن است عمل را دوبار اجرا کند (اگر فقط پاسخ گم شده باشد)؛ مگر قراردادِ idempotence و شناسه‌های یکتا داشته باشی (فصل ۱۲).
  • latency شبکه متغیر است: از کمتر از یک میلی‌ثانیه تا چند ثانیه، برخلافِ تابعِ محلی.
  • نمی‌توانی pointer بدهی؛ همه‌چیز باید encode شود — برای داده‌های بزرگ و objectهای mutable دردسرساز است.
  • کلاینت و سرور ممکن است زبان‌های متفاوت داشته باشند و ترجمهٔ نوع‌ها (مثل مشکلِ اعدادِ بزرگ‌تر از 2^53 در JavaScript) زشت شود.
  • A local call is predictable; a network request is unpredictable (lost packets, a slow far machine).
  • A local call’s outcome is a value or an exception; a network request has a third ending: timeout with no result — you don’t know if it ran at all.
  • Retrying may run the action twice (if only the response was lost); unless you hold an idempotence contract and unique IDs (Chapter 12).
  • Network latency varies: from under a millisecond to seconds, unlike a local function.
  • You can’t pass pointers; everything must be encoded — painful for big data and mutable objects.
  • Client and server may speak different languages, and type translation gets ugly (like the bigger-than-2^53 numbers problem in JavaScript).
بخشی از جذابیتِ REST همین است که انتقالِ state روی شبکه را فراتر از صدازدنِ تابع می‌فهمد. تاریخچه هم پر از شکست‌های پرطمطراق است: EJB و RMI فقط Java، DCOM فقط مایکروسافت، CORBA پیچیده و بدون سازگاری، SOAP/WS-* پیچیده.

Part of REST’s appeal is understanding state transfer over the network as beyond function calls. History is full of grandiose failures too: EJB and RMI for Java only, DCOM for Microsoft only, CORBA complex and incompatible, SOAP/WS-* complex.

load balancing، service discovery و service meshLoad balancing, service discovery and service mesh

کشف سرویس مقصدهای موجود را معرفی می‌کند؛ متعادل‌ساز درخواست‌ها را پخش می‌کند؛ مشِ سرویس بخشی از ارتباطِ میان سرویس‌ها را مدیریت می‌کند.

مثال فنی: کشف سرویس تا انتخاب مقصد

وضعیت اولیه: سرویس انبار سه instance دارد و یکی جابه‌جا شده است.

اجرای مرحله‌به‌مرحله: discovery نشانی‌های معتبر را می‌دهد؛ load balancer یکی را انتخاب می‌کند؛ mesh می‌تواند سیاست اتصال و مشاهده‌پذیری مشترک اجرا کند.

نتیجه و دلیل: هیچ‌کدام idempotency عملیات را خودکار نمی‌سازند؛ retry لایهٔ شبکه باید با معنای درخواست هماهنگ باشد.

راه‌حلچطور کار می‌کندنقطهٔ ضعف / نکته
Load balancer سخت‌افزاریتجهیزِ ویژه در datacenter؛ کلاینت به یک host:port وصل می‌شود و اتصال به سرورها مسیردهی می‌شودتشخیصِ خرابی و جابه‌جاییِ ترافیک؛ هزینهٔ سخت‌افزار
Load balancer نرم‌افزاری (NGINX، HAProxy)همان کار، به‌صورت برنامه روی سخت‌افزارِ معمولیساده و رایج برای deploymentهای متوسط
DNSیک نامِ دامنه ← چند IP؛ لایهٔ شبکهٔ کلاینت یکی را انتخاب می‌کندتغییرات کند منتشر می‌شوند و cache می‌شوند؛ با start/stop مکررِ سرورها IPهای کهنه می‌بینی
سیستمِ service discovery (etcd، ZooKeeper)instance خودش را با host:port و metadata (مالکیتِ shard، دیتاسنتر) ثبت می‌کند و heartbeat می‌فرستد؛ کلاینت فهرستِ endpointها را می‌گیردمحیط‌های پویاتر؛ تصمیمِ load-balancingِ هوشمندانه‌تر (فصل ۷)
service mesh (Istio، Linkerd)load balancer نرم‌افزاری + discovery، به‌صورت کتابخانهٔ درون‌process یا sidecar در دو طرف؛ رمزنگاری در سطحِ load balancerپیچیده ولی observability قوی؛ رایج در محیط‌های Kubernetes
FixHow it worksWeakness / note
Hardware load balancerSpecial datacenter gear; the client connects to one host:port and connections route to serversFailure detection and traffic shifting; hardware cost
Software load balancer (NGINX, HAProxy)The same job as a program on commodity hardwareSimple and common for mid-size deployments
DNSOne domain name → several IPs; the client’s network layer picks oneChanges propagate slowly and get cached; with frequent server start/stops you see stale IPs
Service discovery (etcd, ZooKeeper)Each instance registers itself with host:port and metadata (shard ownership, datacenter) and heartbeats; the client fetches the endpoint listMore dynamic environments; smarter load-balancing calls (Chapter 7)
Service mesh (Istio, Linkerd)Software load balancer + discovery as an in-process library or sidecar on both sides; encryption at the load-balancer levelComplex but strong observability; common in Kubernetes environments
سازگاریِ RPC دقیقاً چه می‌خواهد؟What exactly does RPC compatibility need?
چون می‌توان فرض کرد اول همهٔ سرورها و بعد کلاینت‌ها به‌روز می‌شوند: روی requestها فقط backward و روی responseها فقط forward لازم است. gRPC و Avro RPC قواعدِ همان قالبِ کدگذاری‌شان را ارث می‌برند؛ در REST/JSON معمولاً «افزودنِ پارامترِ اختیاریِ request» و «افزودنِ فیلدِ response» سازگار شمرده می‌شوند. اما چون RPC اغلب از مرزِ سازمان می‌گذرد، کنترلی روی کلاینت‌ها نیست و سازگاری باید شاید تا ابد نگه داشته شود (یا چند نسخهٔ API کنار هم نگه داشته شود). برای نسخه‌بندی هم توافقِ جهانی نیست: شماره در URL، هدرِ Accept، یا نسخهٔ هر کلاینت در سرور.

Since we may assume servers upgrade first, clients after: requests need only backward, responses only forward. gRPC and Avro RPC inherit their encoding format’s rules; in REST/JSON “adding an optional request parameter” and “adding a response field” usually count as compatible. But since RPC often crosses org boundaries there is no control over clients, and compatibility may need keeping forever (or several API versions side by side). Versioning has no global agreement either: a number in the URL, the Accept header, or each client’s version on the server.

۴.۴ workflow و اجرای پایدار (Durable Execution) + نمودار ۵4.4 Workflows and durable execution + Diagram 5

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

مثال فنی: ادامهٔ workflow بعد از crash

وضعیت اولیه: workflow مرحلهٔ رزرو و سپس صدور رسید دارد؛ رزرو با شناسهٔ r7 موفق شده است.

اجرای مرحله‌به‌مرحله: engine پیشرفت را پایدار می‌کند؛ بعد از crash از تاریخچه ادامه می‌دهد، اما فعالیتی که نتیجه‌اش ثبت نشده ممکن است دوباره فراخوانی شود.

نتیجه و دلیل: اثر بیرونی رزرو باید با r7 تکرارپذیرِ امن باشد؛ دوام workflow به‌معنی اجرای فیزیکی یک‌بارهٔ همهٔ APIها نیست.

The book’s example: a payment that must charge the card and deposit money to the bank account — separate services for fraud detection, card connectivity and bank connectivity. Running one payment = several service calls; this sequence is a workflow, each step a task (Temporal calls them activities; some say durable functions). Workflows are usually graphs of tasks, defined in a general language, a DSL, or BPEL.

  • workflow engine تصمیم می‌گیرد هر task کِی و روی کدام ماشین اجرا شود، با خرابی چه کند و چقدر موازی‌سازی کند؛ از یک orchestrator (زمان‌بندی) و یک یا چند executor (اجرا) تشکیل شده. اجرا با trigger شروع می‌شود: زمان‌بندی، سرویسِ وب، یا انسان.
  • خانواده‌ها: ابزارهای داده‌ای مثل Airflow / Dagster / Prefect (هماهنگ‌سازیِ ETL)، ابزارهای گرافیکی مثل Camunda / Orkes با BPMN برای غیرمهندس‌ها، و چارچوب‌های durable execution مثل Temporal / Restate.
  • A workflow engine decides when and on which machine each task runs, what to do on failure, and how much to parallelize; it is an orchestrator (scheduling) plus one or more executors (running). Runs start with a trigger: a schedule, a web service, or a human.
  • Families: data tools like Airflow / Dagster / Prefect (ETL orchestration), graphical tools like Camunda / Orkes with BPMN for non-engineers, and durable execution frameworks like Temporal / Restate.
durable execution چه مشکلی را حل می‌کند؟What problem does durable execution solve?
در معماری سرویسی، نمی‌توانی دو فراخوانیِ کارت و بانک را در یک تراکنشِ پایگاه‌داده‌ای بپیچی؛ ضمناً با درگاه‌های پرداختِ شخصِ ثالث هم کار می‌کنی. اما می‌خواهی هر پرداخت دقیقاً یک‌بار پردازش شود. راه‌حل: چارچوب، همهٔ فراخوانی‌ها و تغییراتِ state را در ذخیره‌سازیِ پایدار (مثل write-ahead log) ثبت می‌کند. اگر taskی شکست بخورد، دوباره اجرا می‌شود اما گام‌هایی که قبلاً موفق بوده‌اند رد می‌شوند و نتیجهٔ قبلی‌شان برمی‌گردد — گویی فراخوانی تکرار نشده.

In a service architecture you can’t wrap the card and bank calls in one database transaction; you also work with third-party payment gateways. But you want each payment processed exactly once. The fix: the framework records every call and state change in durable storage (like a write-ahead log). If a task fails it re-runs, but previously successful steps are skipped with their old results returned — as if the call never repeated.

نمودار ۵ + روایت مدرسDiagram 5 + teacher narration

نمودار ۵ · Sequence — اجرای پایدار و replay در workflow پرداختDiagram 5 · Sequence — durable execution and replay in a payment workflow باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۵Teacher narration

مثال فنی برای خواندنِ نمودار: ادامهٔ workflow بعد از crash

وضعیت اولیه: workflow مرحلهٔ رزرو و سپس صدور رسید دارد؛ رزرو با شناسهٔ r7 موفق شده است.

اجرای مرحله‌به‌مرحله: engine پیشرفت را پایدار می‌کند؛ بعد از crash از تاریخچه ادامه می‌دهد، اما فعالیتی که نتیجه‌اش ثبت نشده ممکن است دوباره فراخوانی شود.

نتیجه و دلیل: اثر بیرونی رزرو باید با r7 تکرارپذیرِ امن باشد؛ دوام workflow به‌معنی اجرای فیزیکی یک‌بارهٔ همهٔ APIها نیست.

شش شرکت‌کننده از چپ به راست: «Checkout» (زیرنویس: «workflow trigger»)، «Workflow Engine» (زیرنویس: «workflow runtime»)، «Durable Log» (زیرنویس: «write-ahead log»)، «Fraud Check» (زیرنویس: «internal service»)، «Card Network» و «Bank Service» (هر دو زیرنویسِ «third-party API»). دو نوارِ عنوان در چپ: «First execution» و «Crash recovery and replay».

Six participants, left to right: Checkout (workflow trigger), Workflow Engine (workflow runtime), Durable Log (write-ahead log), Fraud Check (internal service), Card Network and Bank Service (both third-party API). Two title bands at left: First execution and Crash recovery and replay.

در اجرای اول: «start payment workflow» از Checkout به موتور می‌آید. موتور گام‌ها را یکی‌یکی می‌فرستد: «activity: check fraud» به Fraud Check و پاسخِ «not fraudulent»؛ بعد «append step result» به Durable Log (فلشِ چین‌چین — یعنی هر گامِ موفق اول ثبت می‌شود)؛ بعد «activity: debit card» به Card Network و پاسخِ «charged» و دوباره ثبت در لاگ. گامِ سوم «activity: deposit funds» به Bank Service است که یادداشتِ زیرش می‌گوید: «worker crashes mid-step» — همین‌جا worker می‌میرد.

First run: start payment workflow arrives from Checkout. The engine sends steps one by one: activity: check fraud to Fraud Check, reply not fraudulent; then dashed append step result to the Durable Log (every successful step is recorded first); then activity: debit card to Card Network, reply charged, recorded in the log again. The third step, activity: deposit funds to Bank Service, is annotated worker crashes mid-step — the worker dies right there.

نوارِ دوم، قصهٔ نجات است: «re-execute workflow» (فلشِ چین‌چین از Checkout)، بعد «replay recorded steps» از موتور به Durable Log — موتور می‌بیند check fraud و debit card قبلاً انجام و ثبت شده‌اند. پس فقط گامِ گم‌شده را می‌فرستد: «deposit funds (only missing step)» به Bank Service، پاسخِ «deposited»، و در نهایتِ خوش، «payment complete, exactly once» به Checkout.

Band two is the rescue story: re-execute workflow (dashed arrow from Checkout), then replay recorded steps from the engine to the Durable Log — the engine sees fraud-check and debit-card already done and recorded. So it sends only the missing step: deposit funds (only missing step) to Bank Service, reply deposited, and the happy end payment complete, exactly once to Checkout.

درسِ شکل: «دقیقاً یک‌بار» با تراکنشِ سنتیِ پایگاه‌داده‌ای به دست نمی‌آید؛ با لاگِ پایدار + بازپخش به دست می‌آید. و البته بهایی دارد — همان چیزی که در بخشِ بعد می‌خوانید.

The lesson: “exactly once” doesn’t come from a traditional database transaction; it comes from a durable log + replay. And of course it has a price — which the next part covers.

چالش‌های durable executionDurable-execution challenges

اجرای پایدار باید با تکرار مرحله، تغییر نسخهٔ کد و نتیجهٔ نامشخصِ سرویس بیرونی کنار بیاید.

مثال فنی: Replay با کد عوض‌شده

وضعیت اولیه: workflow قدیمی در تاریخچه ابتدا «رزرو» و سپس «رسید» دارد.

اجرای مرحله‌به‌مرحله: کد تازه اگر بدون نسخه‌بندی ترتیب تصمیم‌ها را عوض کند، replay ممکن است با تاریخچه ناسازگار شود.

نتیجه و دلیل: دادهٔ تصمیم و نسخهٔ منطق باید قابل‌تفسیر بمانند؛ ساعت و تصادف و I/O نباید بی‌قاعده داخل replay تصمیم‌ساز شوند.

  • سرویس‌های بیرونی (مثل درگاهِ پرداختِ شخصِ ثالث) باید هنوز idempotent باشند؛ تو باید با شناسه‌های یکتا جلوی اجرای تکراری را بگیری.
  • چارچوب فراخوانی‌ها را به ترتیب ثبت می‌کند و انتظار دارد اجرای بعدی دقیقاً همان فراخوانی‌ها را به همان ترتیب بکند؛ پس جابه‌جاییِ دو خطِ کد می‌تواند رفتارِ بازپخشِ invocationهای در جریان را مبهم کند. راه‌حلِ امن: تغییرِ کدِ workflow را به‌صورت نسخهٔ جدا deploy کنی تا invocationهای قدیمی با منطقِ قدیم بازپخش شوند.
  • بازپخش باید قطعی (deterministic) باشد: همان ورودی ← همان خروجی. تولیدِ عددِ تصادفی یا خواندنِ ساعتِ سیستم ممنوع؛ چارچوب‌ها نسخه‌های قطعیِ این توابع را می‌دهند و ابزارهایی مثل Workflow Check تخلف را پیدا می‌کنند. (بازگشت به بحثِ قطعیت در فصل ۹.)
  • Outside services (like third-party payment gateways) must still be idempotent; you must block repeat runs with unique IDs.
  • The framework records calls in order and expects the next run to make exactly the same calls in the same order; so swapping two code lines can blur the replay behavior of in-flight invocations. The safe fix: deploy workflow-code changes as a separate version so old invocations replay with old logic.
  • Replay must be deterministic: same input → same output. Random numbers and system-clock reads are banned; frameworks give deterministic versions of these functions, and tools like Workflow Check catch violations. (Back to the determinism debate in Chapter 9.)

۴.۵ معماری event-driven4.5 Event-driven architecture

در معماری رویدادمحور، تولیدکننده خبرِ وقوع یک اتفاق را منتشر می‌کند و مصرف‌کننده‌ها مستقل به آن واکنش نشان می‌دهند. شکل و نسخهٔ رویداد همچنان یک قرارداد مشترک است.

مثال فنی: رویداد مستقل از مصرف‌کننده

وضعیت اولیه: سفارش 42 رویداد OrderPlaced را منتشر می‌کند.

اجرای مرحله‌به‌مرحله: گروه ارسال و گروه تحلیل هر دو آن را می‌گیرند؛ چند worker در گروه ارسال کار همان گروه را تقسیم می‌کنند.

نتیجه و دلیل: pub/sub تکثیر بین گروه‌هاست و queue تقسیم کار داخل گروه؛ دریافت پیام هنوز به کنترل تکرار و خطا نیاز دارد.

  • در این معماری، درخواست یک event یا message است و فرستنده معمولاً منتظر پردازشِ گیرنده نمی‌ماند؛ پیام از یک message broker (event broker، message queue، MOM) می‌گذرد که پیام را موقتاً نگه می‌دارد.
  • مزایای broker نسبت به RPCِ مستقیم: بافر بودن وقتی گیرنده در دسترس نیست یا overload است؛ تحویلِ مجددِ خودکار بعد از crash؛ بی‌نیازی از service discovery؛ ارسالِ یک پیام به چند گیرنده؛ و جداییِ منطقیِ فرستنده و گیرنده (فرستنده فقط publish می‌کند).
  • دو الگوی توزیع: queue (یک مصرف‌کننده از چند مصرف‌کننده پیام را می‌گیرد) و topic / pub-sub (همهٔ subscriberها نسخهٔ خودشان را می‌گیرند).
  • brokerها مدلِ داده تحمیل نمی‌کنند: پیام = بایت‌ها + metadata. رایج: Protobuf/Avro/JSON به‌همراهِ یک schema registry؛ و AsyncAPI معادلِ OpenAPI برای پیام. نمونه‌ها: RabbitMQ، ActiveMQ، NATS، Redpanda، Kafka (متن‌باز) و Kinesis، Azure Service Bus، Cloud Pub/Sub (ابری) — مقایسه در فصل ۱۲.
  • دوامِ پیام: بسیاری روی دیسک می‌نویسند تا با crash از بین نروند؛ برخلافِ پایگاه داده، بسیاری بعد از مصرف خودکار حذف می‌کنند. برای event sourcing به broker نیاز داری که پیام‌ها را تا ابد نگه دارد (فصل ۳).
  • اگر مصرف‌کننده‌ای پیام را دوباره در topic دیگری publish کند، باید فیلدهای ناشناخته را حفظ کند — وگرنه همان بلای شکل ۵-۱ این‌بار وسطِ pipeline می‌آید.
  • actor model: به‌جای thread و قفل و deadlock، منطق در actorها کپسوله می‌شود؛ هر actor state محلیِ خودش را دارد، با پیامِ ناهمزمان حرف می‌زند، یک پیام در هر لحظه پردازش می‌کند و تحویلِ پیام تضمین‌شده نیست. در چارچوب‌های توزیع‌شده مثل Akka، Orleans و Erlang/OTP همان مکانیزم، محلی یا دور، یکسان است (پیام شفاف encode و در آن‌طرف decode می‌شود). location transparency این‌جا بهتر از RPC جواب می‌دهد چون مدل از اول «پیام ممکن است گم شود» را می‌پذیرد. برای rolling upgrade باز هم به کدگذاریِ سازگار نیاز داری.
  • In this architecture a request is an event or message and the sender usually doesn’t wait for the receiver’s processing; the message passes through a message broker (event broker, message queue, MOM) holding it temporarily.
  • Broker advantages over direct RPC: buffering when the receiver is down or overloaded; automatic redelivery after a crash; no service discovery needed; sending one message to several receivers; and logical sender/receiver decoupling (the sender only publishes).
  • Two distribution patterns: queue (one of several consumers takes the message) and topic / pub-sub (every subscriber gets its own copy).
  • Brokers impose no data model: a message = bytes + metadata. Common: Protobuf/Avro/JSON with a schema registry; AsyncAPI as the message-world OpenAPI. Examples: RabbitMQ, ActiveMQ, NATS, Redpanda, Kafka (open source) and Kinesis, Azure Service Bus, Cloud Pub/Sub (cloud) — compared in Chapter 12.
  • Message durability: many write to disk to survive crashes; unlike databases, many auto-delete after consumption. For event sourcing you need a broker keeping messages forever (Chapter 3).
  • If a consumer republishes a message to another topic it must preserve unknown fields — otherwise Figure 5-1’s curse strikes mid-pipeline.
  • Actor model: instead of threads, locks and deadlocks, logic is encapsulated in actors; each actor owns its local state, talks over async messages, processes one message at a time, and delivery isn’t guaranteed. In distributed frameworks like Akka, Orleans and Erlang/OTP the same mechanism works locally or remotely (messages transparently encode here, decode there). Location transparency works better here than in RPC because the model accepts “messages may be lost” from the start. Rolling upgrades still need compatible encoding.
حالتِ جریانچه کسی encode می‌کندچه کسی decode می‌کندسازگاریِ لازم
پایگاه دادهprocessِ نویسندهprocessِ خواننده (شاید نسخه‌ای قدیمی‌تر یا «خودِ آینده»)backward + forward (data outlives code)
REST / RPCکلاینت (request) و سرور (response)سرور (request) و کلاینت (response)backward روی request، forward روی response (سرورها اول)
workflow / durable executionهر task هنگام تکمیلorchestrator هنگام replayقالبِ لاگ باید برای بازپخشِ آینده پایدار بماند؛ کدِ workflow قطعی
broker / actorفرستندهsubscriberها (شاید با نسخه‌های مختلف)backward + forward؛ حفظِ فیلدِ ناشناخته هنگام republish

نقشهٔ فصل — هفت ایده در یک نگاهChapter map — seven ideas at a glance

نقشهٔ فصل ۵ · انتخاب‌های کدگذاری در یک نگاهChapter 5 map · Encoding choices at a glance باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نقشهٔ فصلChapter-map narration

مثال فنی برای خواندنِ نمودار: رکورد قدیمی زیر کد جدید

وضعیت اولیه: نسخهٔ ۱ رکورد {name: "Sara"} نوشته و نسخهٔ ۲ فیلد email اضافه کرده است.

اجرای مرحله‌به‌مرحله: خوانندهٔ تازه برای رکورد قدیمی باید رفتارِ نبود email را بداند؛ خوانندهٔ قدیمی هم باید فیلد تازه را طبق قرارداد تحمل کند.

نتیجه و دلیل: آزمون سازگاری باید هر دو جهت خواندن را پوشش دهد؛ صرفِ موفقیت encode/decode با نسخهٔ یکسان کافی نیست.

گرهٔ میانی «کدگذاری و تکامل» است: از بالا «backward + forward» (با یال «خواندن»)، «قالب‌های متنی» («بایت‌ها») و «پروتوباف + Avro» («تگ‌ها»)؛ در میانه «رجیستری schema» («بررسی») و «ارتقاهای تدریجی» («انتشار»)؛ و در پایین «سرویس‌ها + RPC» («فراخوانی») و «رویدادها + workflowها» («جریان»). پیام نقشه: قالب، قراردادِ خواندنِ «کدِ هنوزنوشته‌نشده» از «دادهٔ دیروز» است.

The hub is “Encoding & Evolution”: on top “Backward + forward” (“reads”), “Text formats” (“bytes”) and “Protobuf + Avro” (“tags”); mid-row the “Schema registry” (“checks”) and “Rolling upgrades” (“ships”); below “RPC + services” (“calls”) and “Events + workflows” (“streams”). The map’s message: a format is the contract letting “code not yet written” read “yesterday’s data.”

Dataflow modeWho encodesWho decodesNeeded compatibility
DatabaseWriter processReader process (maybe older, or “future self”)Backward + forward (data outlives code)
REST / RPCClient (request) and server (response)Server (request) and client (response)Backward on requests, forward on responses (servers first)
Workflow / durable executionEach task on completionOrchestrator on replayLog format must stay stable for future replays; deterministic workflow code
Broker / actorSenderSubscribers (maybe mixed versions)Backward + forward; preserve unknown fields on republish

۵5بستهٔ مصاحبهInterview pack

۵.۱ همه‌چیز در یک نگاه5.1 Everything at a glance

اصطلاحتعریف یک‌خطیکلیدواژهٔ مصاحبه
Encoding / Decodingتبدیلِ object درون‌حافظه به بایت و معکوسserialize/marshal و parse/unmarshal
Backward Compatibilityکدِ جدید، دادهٔ قدیمی را بخواندخواندن از گذشته
Forward Compatibilityکدِ قدیمی، دادهٔ جدید را بخواند/نگه داردنادیده‌گیریِ هوشمندانهٔ افزوده‌ها
Rolling Upgradedeploy تدریجیِ نسخهٔ جدید چند node به چند nodeهم‌زمانیِ نسخه‌ها
Data Outlives Codeداده سال‌ها با فرمتِ قدیمی می‌ماندتعهدِ بلندمدتِ هر قالب
Unknown Field Preservationنگه‌داشتنِ بایت‌های فیلدِ ناشناخته هنگام بازنویسیمانعِ گم‌شدنِ بی‌صدای داده
JSON Schemaزبانِ نوع‌دهی و اعتبارسنجیِ اسناد JSONcontent model باز/بسته، patternProperties
Base64کدگذاریِ بایت‌ها به‌صورت متنحدود ۳۳٪ حجمِ اضافه
MessagePack / CBORکدگذاریِ باینریِ همان مدلِ JSON۶۶ بایت در برابر ۸۱ — بدون schema
Field Tag (Protobuf)شماره‌ای فشرده به‌جای نامِ فیلد در دادهتغییرِ نام آزاد، تغییرِ تگ ممنوع
Varintکدگذاریِ عدد با بایت‌های متغیر۱۳۷۱ در دو بایت
Writer's / Reader's Schemaschemaِ موقعِ نوشتن و schemaِ موردِ انتظارِ خوانندهAvro resolution با نامِ فیلد
Schema Registryفهرستِ نسخه‌های schema با بررسیِ سازگاریConfluent، Apicurio، Espresso
Object Container Fileفایلِ Avro با یک schema در سرآغازdump و بایگانی
IDLزبانِ تعریفِ interface/data برای سرویس و دادهOpenAPI، proto، Avro IDL
Location Transparencyجلوه دادنِ فراخوانیِ دور مثل فراخوانیِ محلیدر RPC گمراه‌کننده، در actorها معقول‌تر
Service Discoveryیافتنِ endpointهای در دسترسِ یک سرویسetcd، ZooKeeper، DNS، sidecar
Durable Executionاجرای workflow با لاگِ پایدار و بازپخشexact-once، determinism
Message Brokerواسطِ نگه‌دارندهٔ موقتِ پیام بین فرستنده و گیرندهqueue در برابر topic/pub-sub
Actor Modelواحدهای هم‌زمان با state محلی و پیامِ ناهمزمانAkka، Orleans، Erlang/OTP
TermOne-line definitionInterview keyword
Encoding / decodingIn-memory object ↔ bytes, and backSerialize/marshal, parse/unmarshal
Backward compatibilityNew code reads old dataReading from the past
Forward compatibilityOld code reads/keeps new dataSmart ignoring of additions
Rolling upgradeGradual node-by-node deploy of the new versionVersion coexistence
Data outlives codeData stays years in its old formatEvery format’s long-term commitment
Unknown-field preservationKeeping unknown fields’ bytes on rewriteStops silent data loss
JSON SchemaJSON documents’ typing and validation languageOpen/closed content model, patternProperties
Base64Encoding bytes as text~33% size overhead
MessagePack / CBORBinary encoding of the same JSON model66 vs 81 bytes — no schema
Field tag (protobuf)A compact number instead of the field name in dataFree renames, frozen tags
VarintVariable-byte number encoding1337 in two bytes
Writer’s / reader’s schemaWrite-time schema vs the reader’s expected schemaAvro resolution by field name
Schema registrySchema-version list with compatibility checksConfluent, Apicurio, Espresso
Object container fileAvro file with one schema up frontDumps and archives
IDLInterface/data definition language for services and dataOpenAPI, proto, Avro IDL
Location transparencyMaking remote calls look like local callsMisleading in RPC, saner in actors
Service discoveryFinding a service’s live endpointsetcd, ZooKeeper, DNS, sidecar
Durable executionWorkflow runs with a durable log and replayExactly-once, determinism
Message brokerTemporary message holder between sender and receiverQueue vs topic/pub-sub
Actor modelConcurrent units with local state and async messagesAkka, Orleans, Erlang/OTP

۵.۲ سوالات پرتکرار مصاحبه (برای دیدن جواب کلیک کن)5.2 Frequent interview questions (click for answers)

۱. backward و forward compatibility یعنی چه و در APIها چطور معنا می‌شوند؟1. What do backward and forward compatibility mean — and in APIs?

عقب‌رو یعنی کدِ تازه دادهٔ قدیمی را بخواند؛ جلو‌رو یعنی کدِ قدیمی با دادهٔ تازه کنار بیاید. در API باید نویسنده و خوانندهٔ درخواست و پاسخ را جدا مشخص کنیم.

مثال فنی: چهار ترکیب در rolling upgrade

وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.

اجرای مرحله‌به‌مرحله: نسخهٔ ۲ رکورد قدیمی را می‌خواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.

نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش می‌دهد.

Backward: new code reads old data/inputs. Forward: old code reads new data/outputs without destroying them. In APIs: for an old client calling a new service, requests must be backward and responses forward; for a new client with an old service, the mirror.

۲. چرا rolling upgrade هر دو نوع سازگاری را لازم دارد؟2. Why does a rolling upgrade need both compatibilities?

هنگامِ انتشارِ تدریجی، همهٔ گره‌ها و کلاینت‌ها هم‌زمان ارتقا پیدا نمی‌کنند؛ دادهٔ قدیمی و تازه هم کنار هم وجود دارد. بنابراین هر دو جهتِ خواندن ممکن است لازم باشد.

مثال فنی: چهار ترکیب در rolling upgrade

وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.

اجرای مرحله‌به‌مرحله: نسخهٔ ۲ رکورد قدیمی را می‌خواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.

نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش می‌دهد.

Because mid-deploy some nodes run new code and some old; data the new version writes may immediately be read by the old (forward), and old data must still be read by the new (backward). Client-side, users control the update timing.

۳. خطرِ شکل ۵-۱ کتاب چیست و چطور رفع می‌شود؟3. What is the book’s Figure 5-1 danger, and the fix?

کدِ قدیمی ممکن است فیلدِ ناشناخته را هنگامِ خواندن حذف کند و بعد با بازنویسی، آن را از دادهٔ اصلی هم پاک کند. حفظِ فیلد یا به‌روزرسانیِ محدود راه‌های جلوگیری‌اند.

مثال فنی: ویرایش نام، حذف email

وضعیت اولیه: نسخهٔ ۲ رکورد {name:Sara,email:a@b} نوشته است؛ نسخهٔ ۱ فقط name را می‌شناسد.

اجرای مرحله‌به‌مرحله: نسخهٔ ۱ کل شیء را می‌خواند، name را عوض می‌کند و بدون email بازمی‌نویسد؛ update محدود به name یا حفظ فیلد ناشناخته جلوی حذف را می‌گیرد.

نتیجه و دلیل: یک read-modify-write قدیمی می‌تواند دادهٔ جدید را پاک کند، حتی وقتی decode بدون خطا بوده است.

Old code decodes a record holding an unknown field, modifies and rewrites it; if the decoder doesn’t keep unknown fields, that field silently vanishes. Fix: schema-driven formats preserving unknown fields’ bytes on round-trip (protobuf does this), or decoding into a structure that explicitly retains them.

۴. چهار مشکلِ اصلیِ کدگذاریِ ویژهٔ زبان (مثل Serializable و pickle) چیست؟4. What are the four core problems of language-native encoding (Serializable, pickle)?

چهار خطرِ مهم: وابستگی به زبان و کلاس‌ها، بازسازیِ ناامنِ اشیا، دشواریِ تغییرِ نسخه و قالبِ نامناسبِ تبادل. این روش‌ها برای ورودیِ نامطمئن به‌ویژه نیازمند احتیاط‌اند.

مثال فنی: نام کلاس، قرارداد پایدار نیست

وضعیت اولیه: فایل ذخیره‌شده به کلاس OldUser در نسخهٔ قبلی برنامه وابسته است.

اجرای مرحله‌به‌مرحله: نسخهٔ جدید کلاس را حذف کرده یا زبان دیگری دارد؛ decoder ویژهٔ زبان ممکن است نتواند شیء را بسازد.

نتیجه و دلیل: داده از عمر یک نسخهٔ کد طولانی‌تر است؛ برای تبادل، schema مستقل و ورودیِ معتبر لازم است.

Language lock-in (other-language reads are hard), the arbitrary-class-instantiation security hole (RCE), versioning/compatibility neglect, and weak performance (Java’s built-in serialization is notorious). Transient uses only.

۵. مشکلِ اعداد در JSON چیست؟5. What’s the numbers problem in JSON?

قالبِ JSON تضمینِ مشترکی برای دقتِ همهٔ خواننده‌ها نمی‌دهد. بعضی محیط‌ها، از جمله عددِ معمولِ JavaScript، همهٔ عددهای صحیحِ بزرگ را دقیق نگه نمی‌دارند.

مثال فنی: شناسهٔ بزرگ در JSON

وضعیت اولیه: شناسهٔ 9007199254740993 به‌صورت JSON number به JavaScript می‌رسد.

اجرای مرحله‌به‌مرحله: تبدیل به Number ممکن است آن را به 9007199254740992 گرد کند؛ قراردادِ رشته‌ای برای شناسه، رقم‌ها را حفظ می‌کند.

نتیجه و دلیل: JSON به‌تنهایی دقتِ همهٔ خواننده‌ها را تضمین نمی‌کند؛ نوع فیلد باید در دو طرف قرارداد روشن باشد.

JSON doesn’t split integers from floats and pins no precision; numbers above 2^53 don’t stay exact in doubles. Example: X returns a post’s 64-bit id as both number and string. XML and CSV are worse: numbers aren’t distinguishable from numeric strings at all.

۶. چرا برای داده‌های باینری در JSON از Base64 استفاده می‌کنند و هزینه‌اش چیست؟6. Why is Base64 used for binary data in JSON — at what cost?

Base64 بایت‌ها را به متنِ قابل‌حمل تبدیل می‌کند. اندازه معمولاً حدودِ یک‌سوم بیشتر می‌شود؛ این کدگذاری است و محرمانگی ایجاد نمی‌کند.

مثال فنی: Base64 برای بایت در JSON

وضعیت اولیه: فایل باینری سه‌بایتی باید در یک فیلد متنی JSON فرستاده شود.

اجرای مرحله‌به‌مرحله: Base64 هر سه بایت را به چهار نویسه تبدیل می‌کند؛ برای ورودی بزرگ، حجم حدود یک‌سوم زیاد می‌شود، جدا از سربار JSON.

نتیجه و دلیل: این روش حمل بایت در رشته را ممکن می‌کند؛ فشرده‌سازی نیست و برای فایل بزرگ شاید مسیر جدا مناسب‌تر باشد.

JSON/XML have no binary strings (unencoded byte sequences); Base64 turns bytes into text, with the schema declaring the value is really Base64. Cost: ~one-third size growth + conversion overhead. True binary formats have a native binary type.

۷. content model باز و بسته در JSON Schema یعنی چه؟7. What are open and closed content models in JSON Schema?

مدلِ باز، فیلدِ اضافه را مجاز می‌داند؛ مدلِ بسته آن را رد می‌کند. این انتخاب بر پذیرشِ نسخهٔ جدید و کشفِ اشتباهِ ورودی اثر دارد.

مثال فنی: Schema باز یا بسته

وضعیت اولیه: قرارداد فقط name را تعریف کرده؛ پیام تازه فیلد email هم می‌فرستد.

اجرای مرحله‌به‌مرحله: با additionalProperties:false در همان شیء، validator فیلد اضافه را رد می‌کند؛ مدل باز می‌تواند آن را بپذیرد.

نتیجه و دلیل: اضافه کردن فیلد همیشه سازگار نیست؛ سیاست خواننده دربارهٔ فیلد ناشناخته تعیین‌کننده است.

Open: undefined fields allowed (additionalProperties: true by default). Closed: only explicit fields. So JSON Schema usually defines what is not allowed rather than what is. With patternProperties + additionalProperties you can also write constraints like “keys digits-only, values strings-only”.

۸. MessagePack چقدر فضا جبران می‌کند و چرا کتاب چندان تحویلش نمی‌گیرد؟8. How much space does MessagePack save — and why doesn’t the book rate it?

در نمونهٔ مشخصِ فصل، MessagePack از ۸۱ به ۶۶ بایت رسیده است؛ این نسبت عمومی نیست. بدون اسکیما، نامِ فیلدها همچنان بخشی از داده‌اند و صرفِ باینری بودن همهٔ سربار را حذف نمی‌کند.

مثال فنی: MessagePack چه چیزی را حذف می‌کند؟

وضعیت اولیه: شیء JSON نام فیلد customer_id را در هر رکورد تکرار می‌کند.

اجرای مرحله‌به‌مرحله: MessagePack نمایش نوع و مقدار را باینری‌تر می‌کند، اما در استفادهٔ map معمول، نام فیلد هنوز همراه هر رکورد است.

نتیجه و دلیل: سود حجم به داده بستگی دارد؛ قالب schemaدار می‌تواند نام‌ها را از wire حذف کند و قرارداد تکامل جدا بدهد.

Example record: 66 vs 81 JSON bytes — ~18% smaller. Schema-less, field names stay in the data; only the JSON model got binarized. A small saving for lost human-readability; with a schema you reach 32–33 bytes.

۹. field tag در Protocol Buffers چیست و چه قوانینی دارد؟9. What is a field tag in Protocol Buffers — what are its rules?

تگ، شمارهٔ هویتِ فیلد در قالبِ سیمی است. نامِ فیلد می‌تواند طبقِ قرارداد عوض شود، اما شمارهٔ حذف‌شده نباید به معنای تازه اختصاص یابد و باید رزرو شود.

مثال فنی: Tag هویت فیلد است

وضعیت اولیه: schema پیام شامل string name = 1 است و بعد string email = 2 اضافه می‌شود.

اجرای مرحله‌به‌مرحله: شمارهٔ ۱ همچنان به name تعلق دارد؛ پس از حذف email، شمارهٔ ۲ برای معنای دیگری دوباره استفاده نمی‌شود.

نتیجه و دلیل: تغییر اسم ظاهری با تغییر هویت wire فرق دارد؛ reuse شماره می‌تواند دادهٔ قدیمی را با معنای اشتباه بخواند.

The number replacing the field’s name in data (the schema’s numbers). Field names may change (they aren’t in the data) but tags never — a tag change invalidates all existing data. Deleted fields’ tags must be reserved against reuse.

۱۰. قواعد تکاملِ schema در protobuf برای افزودن/حذف/تغییر نوع چیست؟10. What are protobuf’s schema-evolution rules for add/remove/retype?

فیلدِ تازه شمارهٔ تازه می‌خواهد؛ شمارهٔ حذف‌شده رزرو می‌شود. تغییرِ نوع باید طبقِ قواعدِ سازگاریِ سیمی بررسی شود و حتی تغییرِ مجاز می‌تواند در بعضی مقدارها داده را محدود کند.

مثال فنی: تکامل protobuf با شمارهٔ ثابت

وضعیت اولیه: فیلد string name = 1 منتشر شده و پیام‌های قدیمی هنوز وجود دارند.

اجرای مرحله‌به‌مرحله: افزودن فیلد تازه با شمارهٔ آزاد می‌تواند با خوانندهٔ سازگار کار کند؛ حذف باید شماره و نام لازم را reserved کند؛ تغییر نوع باید قواعد wire و معنای مقدار را رعایت کند.

نتیجه و دلیل: تبدیل همان شماره از رشته به معنای عددیِ نامرتبط، مهاجرت امن نیست؛ حفظ شماره به‌تنهایی حفظ معنا را تضمین نمی‌کند.

Add with a fresh tag: both backward and forward (old code skips and preserves unknowns; new code defaults old data). Remove: add’s mirror, tags never reused. Retype: possible for some types but truncation-risky (e.g. a 64-bit value read into old code’s 32-bit variable).

۱۱. Avro هنگام decode از چه چیزی استفاده می‌کند و writer's schema از کجا می‌آید؟11. What does Avro decode with — and where does the writer’s schema come from?

Avro اسکیمای نویسنده و خواننده را تطبیق می‌دهد. اسکیمای نویسنده می‌تواند در فایل باشد یا با شناسه از مخزنِ قرارداد گرفته شود؛ دادهٔ باینری به‌تنهایی کافی نیست.

مثال فنی: پیدا کردن schema نویسنده

وضعیت اولیه: پیام Avro شناسهٔ schema برابر 23 دارد و بایت‌های رکورد را حمل می‌کند.

اجرای مرحله‌به‌مرحله: مصرف‌کننده schema ۲۳ را از registryِ مورد توافق می‌گیرد و همراه reader schema به decoder می‌دهد.

نتیجه و دلیل: داشتن فقط reader schema برای تفسیر همهٔ بایت‌ها کافی نیست؛ شناسه و دسترس‌پذیری schemaهای قدیمی باید حفظ شود.

Two schemas: writer’s (at write time) and reader’s (current code’s expectation); resolution pairs fields by name. Writer’s-schema sources: once up front in a big file (object container), a per-record version number + registry (like Confluent for Kafka), or connection-open negotiation (Avro RPC).

۱۲. قواعدِ Avro دربارهٔ مقدارِ پیش‌فرض (default) چیست؟12. What are Avro’s default-value rules?

اگر خواننده فیلدی بخواهد که نویسنده نداشته، مقدارِ پیش‌فرضِ خواننده لازم است. حذفِ فیلد از نویسنده برای خوانندهٔ قدیمی نیز به قراردادِ همان خواننده وابسته است؛ default به‌معنی اختیاری شدنِ نوشتن نیست.

مثال فنی: Resolution در Avro

وضعیت اولیه: writer فقط name دارد؛ reader تازه فیلد country با default برابر IR اضافه کرده است.

اجرای مرحله‌به‌مرحله: هنگام خواندن رکورد قدیمی، reader مقدارِ غایب country را از default می‌گیرد.

نتیجه و دلیل: default برای حلِ نبود فیلد در دادهٔ نویسنده است؛ جای الزام نوشتن فیلد در schema نویسنده را نمی‌گیرد.

منبعِ توضیحِ فنی: قواعدِ رسمیِ تطبیقِ اسکیمای Avro

Only a field with a default may be added or removed. Adding a default-less field breaks backward (the new reader doesn’t know what to put); removing one breaks forward (the old reader has no value).

۱۳. چرا در Avro برای nullable بودن باید union نوشت؟13. Why must Avro nullability use a union?

برای پذیرفتنِ null، نوع باید آن را صریحاً شامل شود. Union فهرستِ نوع‌های مجاز است؛ مقدارِ پیش‌فرض و مجاز بودنِ null دو موضوعِ مرتبط ولی جدا هستند.

مثال فنی: Nullable در Avro

وضعیت اولیه: email ممکن است رشته یا null باشد.

اجرای مرحله‌به‌مرحله: schema باید union مانند ["null","string"] را مجاز کند؛ برای رکوردهای قدیمی می‌توان default معتبر null در reader تعریف کرد.

نتیجه و دلیل: غایب بودن فیلد، null بودن مقدار و default داشتن سه مفهوم جدا هستند و باید جدا طراحی شوند.

In Avro, null isn’t everything’s default; you must explicitly list a field’s possible types: union { null, long }. And you may default to null only when null is the union’s first branch. This explicitness blocks “can this field really be null?” bugs.

۱۴. تغییر نامِ فیلد در Avro چرا فقط backward است؟14. Why is an Avro field rename backward-only?

Alias در اسکیمای خوانندهٔ تازه می‌تواند نامِ قبلیِ نویسنده را بشناسد. خوانندهٔ قدیمی الزاماً از نامِ تازه خبر ندارد؛ جهتِ سازگاری را باید جدا بررسی کرد.

مثال فنی: تغییر نام با alias

وضعیت اولیه: writer قدیمی فیلد surname نوشته و reader جدید family_name با alias برابر surname دارد.

اجرای مرحله‌به‌مرحله: reader جدید نام قدیمی را تطبیق می‌دهد؛ reader قدیمی alias تازه را نمی‌شناسد و خواندن جهت معکوس خودکار حل نمی‌شود.

نتیجه و دلیل: این تغییر می‌تواند backward باشد بی‌آنکه forward باشد؛ هر دو جهت باید مستقل آزموده شوند.

The reader’s schema may alias the old name, so data the old writer wrote under the stale name still reads (backward ✓). But the old writer doesn’t know the fresh name, and data the new writer wrote under it won’t pair up (forward ✗). Adding a union branch shares this verdict.

۱۵. مزایای قالب‌های باینریِ مبتنی بر schema نسبت به JSON چیست؟15. What advantages do schema-driven binary formats have over JSON?

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

مثال فنی: مقایسهٔ قالب روی همان رکورد

وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب می‌نویسیم.

اجرای مرحله‌به‌مرحله: JSON نام فیلدها را متنی حمل می‌کند؛ قالب مبتنی بر schema می‌تواند شناسه یا جایگاه فیلد را به‌کار گیرد.

نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن به‌تنهایی تضمین کمترین حجم یا بهترین تکامل نیست.

Denser (field names drop out); the schema is always-current documentation (decoding needs it); a registry lets you test a change’s compatibility before deploy; and they generate code for statically typed languages. Price: not human-readable without tools.

۱۶. «data outlives code» یعنی چه و چه نتیجه‌ای دارد؟16. What does “data outlives code” mean — what follows?

عمرِ داده معمولاً از یک نسخهٔ کد بیشتر است. ارتقای برنامه فایل‌ها و رکوردهای قدیمی را خودکار به شکلِ تازه تبدیل نمی‌کند؛ خواندن یا مهاجرتِ آن‌ها باید طراحی شود.

مثال فنی: سه مسیر همان داده

وضعیت اولیه: تغییر نام کاربر می‌تواند از رکورد پایگاه داده، پاسخ API یا پیام رویداد خوانده شود.

اجرای مرحله‌به‌مرحله: پایگاه داده نسخه‌های قدیمی را نگه می‌دارد؛ API با کلاینت‌های قدیمی سروکار دارد؛ صف ممکن است پیام قدیمی را دوباره تحویل دهد.

نتیجه و دلیل: هر مسیر پنجرهٔ سازگاری خودش را دارد؛ پایان انتشار سرورها لزوماً پایان دادهٔ قدیمی نیست.

Code deploys in minutes, but years-old data keeps its write-day format. So migrating all data is expensive and engines defer it (compaction in LSM, nullable columns in SQL); every data format must be designed to be “read by code not yet written”.

۱۷. سه تفاوتِ محوریِ فراخوانیِ شبکه‌ای با فراخوانیِ محلی چیست؟17. What are three core network-call vs local-call differences?

فراخوانیِ شبکه ممکن است به مقصد نرسد، نتیجه‌اش پس از timeout نامشخص بماند و تأخیرش بسیار متغیر باشد. همچنین تبادلِ قالب میان برنامه‌ها لازم است؛ مشابه بودنِ ظاهرِ RPC این تفاوت‌ها را حذف نمی‌کند.

مثال فنی: RPC ناموفق یا پاسخ نامعلوم؟

وضعیت اولیه: کلاینت CreateOrder(request_id=r7) را می‌فرستد؛ سرور سفارش را ثبت می‌کند، ولی پاسخ نمی‌رسد.

اجرای مرحله‌به‌مرحله: retry با همان شناسه به همان نتیجه وصل می‌شود؛ retry با شناسهٔ تازه می‌تواند سفارش دوم بسازد.

نتیجه و دلیل: برخلاف فراخوانی محلی، timeout شبکه نتیجهٔ عملیات دوردست را مشخص نمی‌کند؛ API باید این ابهام را مدیریت کند.

Unpredictability (network and far-machine failures); the third ending “timeout with no result” (did it run?) demanding idempotence for retries; and wildly varying latency. Plus no pointers and ugly cross-language type translations. So RPC shouldn’t pose too hard as a local function.

۱۸. چرا در RPC فقط backward برای request و forward برای response کافی است؟18. Why is backward-on-request + forward-on-response enough in RPC?

این دو جهت فقط با فرضِ مشخصِ ترتیبِ ارتقا کافی‌اند: سرورِ جدید درخواستِ قدیمی را می‌خواند و کلاینتِ قدیمی پاسخِ تازه را. اگر کلاینتِ تازه با سرورِ قدیمی کار کند، جهت‌های دیگر هم مطرح‌اند.

مثال فنی: API با سرور تازه

وضعیت اولیه: کلاینت قدیمی درخواستِ نسخهٔ ۱ می‌فرستد و پاسخ تازه را دریافت می‌کند.

اجرای مرحله‌به‌مرحله: سرور باید request قدیمی را بخواند؛ کلاینت باید response جدید را تحمل کند. اگر سرورهای قدیمی هنوز فعال‌اند، request تازه هم باید برای آن‌ها قابل‌قبول بماند.

نتیجه و دلیل: جهت سازگاری به نویسنده و خوانندهٔ هر پیام بستگی دارد؛ یک حکم برای همهٔ rolloutها کافی نیست.

Because the operational assumption is servers upgrade first, clients after; so requests are always read by the newer server (backward) and responses by the older client (forward). In cross-org APIs there’s no client control, so compatibility may need keeping forever, or parallel API versions.

۱۹. تفاوتِ queue و topic (pub/sub) چیست؟19. What’s the queue vs topic (pub/sub) difference?

در صفِ کاری، یک پیام به یک مصرف‌کننده برای انجامِ کار واگذار می‌شود. در انتشار–اشتراک، گروه‌های مستقل همان رویداد را دریافت می‌کنند؛ داخلِ هر گروه می‌توان بار را تقسیم کرد.

مثال فنی: رویداد مستقل از مصرف‌کننده

وضعیت اولیه: سفارش 42 رویداد OrderPlaced را منتشر می‌کند.

اجرای مرحله‌به‌مرحله: گروه ارسال و گروه تحلیل هر دو آن را می‌گیرند؛ چند worker در گروه ارسال کار همان گروه را تقسیم می‌کنند.

نتیجه و دلیل: pub/sub تکثیر بین گروه‌هاست و queue تقسیم کار داخل گروه؛ دریافت پیام هنوز به کنترل تکرار و خطا نیاز دارد.

In a queue, only one consumer takes each message (load splitting). In a topic, the broker delivers a copy to every subscriber (fan-out). Brokers usually impose no data model and keep a schema registry for compatibility; a consumer republishing a message must preserve unknown fields.

۲۰. durable execution چطور «دقیقاً یک‌بار» می‌دهد و چه محدودیت‌هایی دارد؟20. How does durable execution deliver “exactly once” — and its limits?

موتورِ اجرای پایدار پیشرفت را ذخیره و پس از توقف ادامه می‌دهد. تضمینِ اثرِ یک‌باره در مقصدِ بیرونی به همکاریِ مقصد، تراکنش یا شناسهٔ جلوگیری از تکرار وابسته است.

مثال فنی: ادامهٔ workflow بعد از crash

وضعیت اولیه: workflow مرحلهٔ رزرو و سپس صدور رسید دارد؛ رزرو با شناسهٔ r7 موفق شده است.

اجرای مرحله‌به‌مرحله: engine پیشرفت را پایدار می‌کند؛ بعد از crash از تاریخچه ادامه می‌دهد، اما فعالیتی که نتیجه‌اش ثبت نشده ممکن است دوباره فراخوانی شود.

نتیجه و دلیل: اثر بیرونی رزرو باید با r7 تکرارپذیرِ امن باشد؛ دوام workflow به‌معنی اجرای فیزیکی یک‌بارهٔ همهٔ APIها نیست.

By recording every call and state change in durable storage and replaying the workflow after failure: successful steps are skipped with old results returned. Limits: outside services must be idempotent with unique IDs; call order must not change (code change = a separate version); code must be deterministic (no RNG, no system clock).

5.3اشتباهات رایج در جواب‌های مصاحبه5.3 Common interview mistakes

  • جا به جا گفتنِ backward و forward؛ قاعده: همیشه از خودت بپرس «چه کسی دارد چه چیزی را می‌خواند؟»
  • فکر کردنِ «JSON که schema ندارد، پس به تکاملِ فکر نمی‌خواهد» — داده‌های JSON هم سال‌ها می‌مانند و فیلدِ اضافه‌شده ممکن است توسط کدِ قدیمی پاک شود.
  • تغییر دادنِ tag در protobuf یا بازاستفاده از tagِ فیلدِ حذف‌شده — یعنی نابودیِ داده‌های موجود.
  • افزودنِ فیلدِ بدونِ default در Avro و تعجب از شکستنِ backward compatibility.
  • پیش‌فرض گرفتنِ «حفظِ فیلدهای ناشناخته» در همهٔ کتابخانه‌ها؛ این رفتار باید صریح بررسی و آزمایش شود.
  • یکی دانستنِ «باینری = فشرده»؛ MessagePack فقط ۶۶ در برابر ۸۱ بایت است؛ فشردگیِ واقعی از حذفِ نامِ فیلدها با schema می‌آید.
  • تلقیِ RPC مثل تابعِ محلی و فراموش کردنِ timeout، retry و idempotence.
  • جابه‌جاییِ ترتیبِ activityها در workflowِ durable و انتظارِ بازپخشِ سالم (یا فراموش کردنِ قطعیت).
  • فراموش کردنِ لزومِ idempotent بودنِ APIهای شخصِ ثالث در معماریِ event-driven و workflow.
  • Swapping backward and forward; the rule: always ask “who is reading what?”
  • Thinking “JSON has no schema, so no evolution thought needed” — JSON data lives for years too, and an added field may be wiped by old code.
  • Changing a protobuf tag or reusing a deleted field’s tag — destroying existing data.
  • Adding a default-less field in Avro, then wondering why backward compatibility broke.
  • Assuming “unknown-field preservation” in every library; the behavior must be checked and tested explicitly.
  • Equating “binary = compact”; MessagePack is only 66 vs 81 bytes — real density comes from dropping field names with a schema.
  • Treating RPC like a local function and forgetting timeout, retry and idempotence.
  • Reordering activities in a durable workflow and expecting healthy replay (or forgetting determinism).
  • Forgetting that third-party APIs must be idempotent in event-driven and workflow designs.

۵.۴ چک‌لیست مرور ۵ دقیقه‌ای قبل از مصاحبه5.4 Five-minute pre-interview checklist

  • تعریفِ backward/forward + ترجمه‌اش به request/response در API.
  • قصهٔ شکل ۵-۱ را در ۳ جمله تعریف کن: چرا فیلدِ ناشناخته گم می‌شود و چطور جلویش را می‌گیریم.
  • سه خانوادهٔ قالب و قضاوتِ نهایی: ویژهٔ زبان (ممنوع)، متنی (تبادلِ انسان‌خوان)، باینریِ schema-driven (فشرده و تکامل‌پذیر).
  • قواعدِ تکاملِ protobuf (تگ ثابت، تغییرِ نام آزاد، reserve کردن) و Avro (default اجباری، union برای null، alias فقط backward).
  • سه زمینهٔ منبعِ writer's schema در Avro + مفهومِ resolution (ignore / default).
  • «data outlives code» و تکلیفِ سازگاری در چهار حالتِ جریان داده (پایگاه داده، RPC، workflow، broker) — جدولِ بخش ۴ را از حفظ بگو.
  • پنج تفاوتِ RPC با فراخوانیِ محلی + سه محدودیتِ durable execution (idempotence، ترتیب، determinism).
  1. Backward/forward definitions + their request/response translation in APIs.
  2. Tell Figure 5-1’s story in 3 sentences: why the unknown field vanishes and how to stop it.
  3. The three format families and the final verdict: language-native (banned), textual (human-readable exchange), schema-driven binary (compact and evolvable).
  4. Protobuf evolution rules (frozen tags, free renames, reserving) and Avro’s (mandatory defaults, unions for null, backward-only aliases).
  5. Avro’s three writer’s-schema sources + resolution (ignore / default).
  6. “Data outlives code” and compatibility duty in the four dataflow modes (database, RPC, workflow, broker) — recite section 4’s table.
  7. Five RPC-vs-local differences + three durable-execution limits (idempotence, order, determinism).

۶6تمرین‌های مصاحبه و آزمایشگاهInterview exercises and hands-on labs

این تمرین‌ها برای مسیر Backend / Distributed Systems در ۱۴ اکتبر ۲۰۲۶ بازبینی شدند. سطح مربوط به نسخهٔ مصاحبه‌ای است؛ زمان آزمایشگاه جداست.Reviewed on 14 October 2026 for Backend / Distributed Systems. Level refers to the interview brief; full-lab time is separate. روش ارزیابی و مسیر مطالعهMethod and study path

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

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

wire format برای protocol/storage آموزشی است؛ حفظ ۶۶ و ۳۳ بایت اولویت عمومی ندارد. شرط حفظ unknown field باید دقیق باشد.Understanding compatibility is more valuable than memorizing byte totals.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

با یک رکورد کوچک تفاوت 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.

معیار پاسخ خوبWhat a good answer demonstrates

  • tag و varint را می‌فهمد، عدد حفظ نمی‌کندTag/varint understanding
  • runtime و نوع تبدیل را مشخص می‌کندSpecified conversion/runtime
  • wire compatibility را از API compatibility جدا می‌کندWire versus application compatibility

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

آزمایشگاه تکمیلی · زمان تقریبی ۱–۲ ساعتExtended hands-on laboratory · estimated 1–2 hours
رکوردِ {"userName": "Martin", "favoriteNumber": 1337, "interests": ["daydreaming", "hacking"]} را بردار. اول با قواعدِ MessagePack (بایتِ نوع + طول) و بعد با قواعدِ varint و tagِ پروتوباف (که در بخش ۳.۴ دیدی) کدگذاریِ دستی کن و بایت‌ها را بشمار؛ باید تقریباً به ۶۶ و ۳۳ بایت برسی. سپس یک فیلدِ تازه (email) با تگ ۴ اضافه کن و نشان بده کدِ قدیمی‌ات (بدونِ دانستنِ تگ ۴) چطور رکورد را round-trip می‌کند بدون آنکه داده گم شود.

Take the record {"userName": "Martin", "favoriteNumber": 1337, "interests": ["daydreaming", "hacking"]}. First hand-encode it with MessagePack rules (type + length bytes), then with protobuf’s varint and tag rules from section 3.4, and count the bytes; you should land near 66 and 33. Then add a fresh field (email) with tag 4 and show how your old code (unaware of tag 4) round-trips the record with no loss.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Protobuf · proto3 language guide · Atlassian · engineering interview handbook

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

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

جهت writer→reader، فرمت binary/JSON، tag، default و alias مشخص‌اند؛ parse شدن با حفظ همهٔ مقدارها یکسان نیست.Name writer→reader direction, binary/JSON format, tags, defaults and aliases; parseability differs from preserving all values.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

برای 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.

معیار پاسخ خوبWhat a good answer demonstrates

  • backward یعنی reader جدید/writer قدیم را روشن می‌کندExplicit writer/reader direction
  • rename Avro را خودکار دوطرفه فرض نمی‌کندAsymmetric rename handling
  • packed numeric و lossy string repeated را می‌بیندPacking and lossy repeated fields

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

آزمایشگاه تکمیلی · زمان تقریبی ۲–۴ ساعتExtended hands-on laboratory · estimated 2–4 hours

برای پروفایل کاربر schemaهای Avro و Protobuf با نسخهٔ runtime مشخص بنویس. افزودن فیلد، rename و singular→repeated را در هر دو جهت old writer→new reader و new writer→old reader آزمایش کن. در Avro، reader default و جهت alias را صریح بنویس؛ nullable بودن به‌تنهایی default ایجاد نمی‌کند. در Protobuf، tag را ثابت نگه دار و تفاوت binary با JSON، حفظ unknown field در binary round-trip و حذف آن در JSON یا کپی field-by-field را بسنج. برای cardinality، نوع scalar/message و packed بودن را تعیین کن و فقط به parse موفق اکتفا نکن: همهٔ مقدارها باید بررسی شوند. مسیر امن را با فیلد و tag تازه، دورهٔ dual-read/write و آزمون rollback طراحی کن؛ معیار قبولی «حفظ معنا» است.

Write Avro and Protobuf user-profile schemas with named runtime versions. Test field addition, rename and singular→repeated in both old-writer→new-reader and new-writer→old-reader directions. State Avro reader defaults and alias direction; nullable alone supplies no default. For Protobuf retain tags and compare binary with JSON, including unknown-field binary round-trips versus JSON or field-by-field copying. State scalar/message type and packed encoding before assessing cardinality changes; verify every value, not just successful parsing. Design a new-field/new-tag migration with dual-read/write and rollback tests. Semantic preservation is the acceptance criterion.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Protobuf · proto3 language guide · Avro 1.12 · schema resolution

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

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

durable workflow، replay و side effect برای backend جدی ارزشمندند؛ باید تصمیم قطعی و نسخه‌بندی invocation روشن باشد.Durable replay is relevant when versioning and side effects are explicit.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

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.

معیار پاسخ خوبWhat a good answer demonstrates

  • شناسه بین retry ثابت استStable retry IDs
  • پرداخت/ارسال بیرونی با قرارداد idempotency پوشش داده شدهExternal idempotency contract
  • کد جدید replay تاریخچه قدیم را نمی‌شکندVersion-safe replay

پیگیری مصاحبه:Interview follow-up: provider ارسال idempotency key ندارد؛ تضمینت چه تغییری می‌کند؟The shipping provider has no idempotency key. How does that change your guarantee?

آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours
یک workflow سه‌مرحله‌ایِ «ثبت‌سفارش» (موجودی، پرداخت، ارسال) طراحی کن که قابلِ بازپخش باشد: برای هر گام شناسهٔ idempotency تعریف کن، مشخص کن چه چیزی در لاگِ پایدار ثبت می‌شود، و بنویس اگر مرحلهٔ سوم دوبار اجرا شود چه اتفاقی می‌افتد. در پایان، سه تغییرِ کد را که بازپخشِ invocationهای در جریان را می‌شکنند فهرست کن و برای هرکدام راه‌کارِ «نسخهٔ جدا» را شرح بده.

Design a replayable three-step “place-order” workflow (inventory, payment, shipping): define an idempotency ID per step, pin down what lands in the durable log, and write what happens if step three runs twice. Finish by listing three code changes that break in-flight replay, with the “separate version” remedy for each.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · AWS · safe retries with idempotent APIs · Atlassian · engineering interview handbook