Course-এর সূচি

2.10

Schema Evolution ও Compatibility

পুরোনো reader বা writer না ভেঙে message-এর format বদলানো: backward, forward আর full compatibility।

40 মিনিট পড়া + Animation

টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।

এই Lesson শেষে তুমি পারবে

  • বোঝাতে, Distributed System-এ একটা Message বা Record-এর Format বদলানো কেন কঠিন: Rolling Deploy, আলাদা আলাদা Team, Rollback, আর বছরের পর বছর টিকে থাকা Data।
  • Backward, Forward আর Full Compatibility (আর এদের Transitive রূপ) Writer Schema আর Reader Schema-র চোখ দিয়ে একদম নিখুঁতভাবে বলতে।
  • প্রতিটা Compatibility মোডে কে আগে Deploy হবে ঠিক করতে, আর ক্রম ভুল হলে সামলে উঠতে।
  • Protobuf (Field Number, reserved, Type বদল, Enum), Avro (Default, Alias, Union, Promotion), JSON (Tolerant Reader, JSON Schema) আর Thrift-এর Evolution-এর নিয়ম খাটাতে।
  • Expand–migrate–contract দিয়ে কোনো Outage ছাড়াই Field-এর নাম বদলাতে, Type বদলাতে বা Field বাদ দিতে।
  • Schema Registry কীভাবে Compatibility চেক করে আর প্রতিটা Record-এর সামনের ৫ Byte-এর মানে কী, আর Event-sourced System কীভাবে Upcaster দিয়ে Event-এর Version সামলায়, তা বলতে।
  • Compatibility চেক, Breaking-change Linter আর Consumer-driven Contract Test দিয়ে CI-তেই ভাঙা পরিবর্তন ধরতে।

#এই Lesson কোথায় বসে

Lesson ২.৯-এ দেখেছি Data কীভাবে Byte হয়: JSON আর XML-এ প্রতিটা Record-এ Field-এর নাম থাকে, Protobuf আর Thrift-এ থাকে Field Number, আর Avro-তে শুধু মান, পড়তে লাগে Writer Schema। এই Lesson-এর বিষয় হলো সময়: পুরোনো Byte আর পুরোনো Program যখন এখনো আছে, তখন Format বদলালে কী হয়।

দুটো Program-এর মাঝের Format একটা চুক্তি, ঠিক যেমন Client আর Server-এর মাঝে Request আর Response-এর Format একটা চুক্তি (Lesson ১.১)। চুক্তির এক পাশ বদলানো সহজ। অন্য পাশ চালু রেখেই, কিছু বন্ধ না করে বদলানো: এটাই পুরো Lesson-এর বিষয়। Lesson ১.২-এ Rolling Deploy দেখেছি (Server আসলে কী), আর Lesson ১.১৩-এ তার পেছনের Load Balancer (Web আর App Server): ঠিক এই Deploy-এর সময়েই পুরোনো আর নতুন Format-এর দেখা হয়।

Schema Evolution মানে Schema বদলাতে থাকলেও Data পড়া-লেখা চালিয়ে যেতে পারা, System বন্ধ না করে, আর আগে থেকে থাকা সব Data নতুন করে না লিখে। Schema হলো Data-র নকশা: কোন কোন Field আছে, তাদের Type কী, কোনগুলো Optional, আর Default কী।

#Schema কেন বদলায়, আর বদলানো কেন কঠিন

System প্রতি সপ্তাহে বদলায়। আজ একটা Table-এ চারটা Field, কাল পাঁচটা হবে, বা একটা কমবে। একটা Event-এ currency যোগ হয়, API Response-এ নতুন একটা Nested Object আসে, একটা ID সংখ্যা থেকে String হয়ে যায়। একটা মেশিনে হলে তুমি Code আর Data একসাথে বদলে দিতে। Distributed System-এ সেটা পারবে না, চারটা কারণে:

পুরোনো আর নতুনের দেখা কোথায়দুই Version একসাথে কেন থাকেকতদিন
Rolling DeployInstance কয়েকটা করে বদলায়, তাই v1 আর v2 পাশাপাশি Traffic সামলায়, একে অন্যের লেখা পড়েকয়েক মিনিট থেকে কয়েক ঘণ্টা
আলাদা Teamএকটা Event-এর Producer আর তার পাঁচটা Consumer আলাদা Team-এর, Release-এর দিনও আলাদাকয়েক দিন থেকে কয়েক মাস
জমা DataDatabase-এর Row, লম্বা Retention-এর Kafka Topic, Data Lake-এর File, Cache-এর Entry, Queue-তে অপেক্ষা করা Message: সব পুরোনো Code লিখেছেকয়েক সেকেন্ড (Queue) থেকে চিরকাল (Event Store, Compacted Topic)
যে Client তোমার হাতে নেইMobile App বছরের পর বছর পুরোনো Version-এ থাকে; Partner একবার Code লিখে তোমার Public API ডাকতেই থাকেকয়েক বছর

আর একটা জিনিস Team-রা ভুলে যায়: Rollback। v2 যদি Data লিখে ফেলে, তারপর তুমি v1-এ ফেরো, তাহলে পুরোনো Code-কে নতুন Data পড়তে হবে।

Rolling Deploy: v1 আর v2 একই Data লেখে আর পড়ে

Deploy চলার সময়, আর যতদিন পুরোনো Data থাকে, দুই Version-এর দেখা হয়। প্রতিটা পরিবর্তন চালাও, দেখো কে এখনো কী পড়তে পারে।

পরিস্থিতি

v2 একটা Default সহ currency যোগ করে। Topic-এ পুরোনো আর নতুন Event মিশে যায়, আর সব Reader সামলে নেয়।

Order svc v1পুরোনো Code২টা Instance
Order svc v2নতুন Code১টা Instance
Orders StoreTopic বা Table
Billing v1অন্য Team
৩টার ১টা Instance এখন v2
ধাপ 1 / 6
1

Rolling Deploy কয়েকটা করে Instance বদলায়, তাই কয়েক মিনিট (বড় Fleet-এ কয়েক ঘণ্টা) v1 আর v2 পাশাপাশি Traffic সামলায়। সব একসাথে বদলে যাওয়ার কোনো মুহূর্ত নেই।

Animation থেকে তিনটা শিক্ষা পুরো বিষয়জুড়ে কাজে লাগবে:

  1. যোগ করা পরিবর্তনই নিরাপদ ধরনের: যুক্তিসঙ্গত Default-সহ একটা নতুন Optional Field।
  2. যেসব Format-এ নামটাই চুক্তি (JSON, Avro), সেখানে নাম বদল মানে একটা মোছা আর একটা যোগ। এক ধাপে করলে কেউ না কেউ ভাঙবেই।
  3. যে Reader Data আবার লিখে রাখে, তাকে না-বোঝা Field-ও রেখে দিতে হবে; না হলে একটা Rollback, বা মিশ্র Fleet-এর একটা v1 Instance, চুপচাপ Data মুছে দেয়।

#Backward, Forward আর Full Compatibility

প্রতিটা Compatibility প্রশ্নে দুটো Schema থাকে: যেটা দিয়ে Data Encode হয়েছিল সেই Writer Schema, আর Code যেটা দিয়ে Decode করে সেই Reader Schema। নামগুলো নতুন Schema-র দিক থেকে দেখা:

 Backward compatibility : new code (new schema) can read OLD data
 Forward compatibility  : old code (old schema) can read NEW data
 Full compatibility     : both backward and forward
  • Backward Compatible পরিবর্তন: নতুন Schema-র Reader পুরোনো Schema-য় লেখা Data পড়তে পারে। সাধারণ উপায়: নতুন Field Optional রাখো বা Default দাও, যাতে সেই Field ছাড়া পুরোনো Record-ও Decode হয়।
  • Forward Compatible পরিবর্তন: পুরোনো Schema-র Reader নতুন Schema-য় লেখা Data পড়তে পারে। এটা চলে কারণ পুরোনো Reader অচেনা Field উপেক্ষা করে। এখানে বিপদের চাল হলো Field মুছে ফেলা: যে পুরোনো Reader-এর ওই Field এখনো লাগে সে ভাঙে, যদি না তার Schema-য় ওই Field-এর Default থাকে।
  • Full Compatible পরিবর্তন: দুটোই। কার্যত শুধু Default আছে এমন Optional Field যোগ বা বাদ দেওয়া।
  • Breaking পরিবর্তন: কোনোটাই না, যেমন একটা Field-এর Type সংখ্যা থেকে String করা।

Schema Registry এগুলোকে মোড বানিয়ে প্রতিটা নতুন Version-এর ওপর চাপিয়ে দেয়:

মোডনতুন Schema-কে…কার সাথে মেলানো হয়কে আগে Deploy
BACKWARD (Confluent-এর ডিফল্ট)আগের Schema-য় লেখা Data পড়তে পারতে হবেশেষ VersionConsumer
BACKWARD_TRANSITIVEআগের সব Schema-য় লেখা Data পড়তে পারতে হবেসব VersionConsumer
FORWARDএমন Data লিখতে হবে যা আগের Schema পড়তে পারেশেষ VersionProducer
FORWARD_TRANSITIVEএমন Data লিখতে হবে যা আগের সব Schema পড়তে পারেসব VersionProducer
FULLআগের Schema-র সাথে দুটোইশেষ Versionযে কেউ
FULL_TRANSITIVEআগের সব Schema-র সাথে দুটোইসব Versionযে কেউ
NONEযা খুশি, কোনো চেক নেইকিছুই নাহাতে হাতে সমন্বয়

প্রতিটা মোড ঠিক কী মানে তা Format-এর ওপর নির্ভর করে (Default ছাড়া Avro Field আর Protobuf Field আলাদা আচরণ করে)। তবে Avro-র জন্য মোটা দাগের নিয়ম: BACKWARD-এ চলে Field মোছা আর Default-সহ Field যোগ; FORWARD-এ চলে Field যোগ আর Default ছিল এমন Field মোছা; FULL-এ চলে শুধু Default-সহ Field যোগ বা মোছা।

Backward, Forward, Full: কোন Version কোন Data পড়ে

নামগুলো নতুন Schema-র চোখ দিয়ে দেখা। Backward: নতুন Schema পুরোনো Schema-য় লেখা Data পড়ে। Forward: পুরোনো Schema নতুন Schema-য় লেখা Data পড়ে।

পরিস্থিতি

v2 Default BDT সহ currency যোগ করে। গত বছর v1 যা লিখেছে, v2 কি তা পড়তে পারে?

WriterProducerv1
জমা DataTopic, Table, File
ReaderConsumer
v1 {orderId, amount}
ধাপ 1 / 4
1

গত বছর Writer v1 ব্যবহার করত। সেই Record-গুলো এখনো জমা আছে।

#Transitive: শুধু শেষটা না, প্রতিটা Version-এর সাথে Compatible

সাধারণ মোডগুলো নতুন Schema-কে শুধু শেষ Version-এর সাথে মেলায়। এতে Topic-এর মাথার কাছ থেকে পড়া Consumer রক্ষা পায়। কিন্তু যে পুরোনো Data পড়ে, সে রক্ষা পায় না: Infinite Retention-এর Topic শুরু থেকে আবার পড়া নতুন Consumer, প্রতিটা Key-র সর্বশেষ Record চিরকাল রাখা Compacted Topic, Event Store, বা Data Lake-এর Avro File। Compatibility Transitive না: v3, v2-এর সাথে Compatible, আর v2, v1-এর সাথে, অথচ v3 আর v1 মেলে না, এমন হতেই পারে।

Transitive Compatibility: নতুন Consumer কয়েক বছরের Event আবার পড়ে

v1 (২০২৪): amount একটা double। v2 (২০২৫): amount বাদ। v3 (২০২৬): amount ফিরে আসে String হয়ে, Default "0"। প্রতিটা ধাপ তার ঠিক আগের ধাপের পাশে ঠিকই দেখাচ্ছিল।

পরিস্থিতি

Registry v3-কে শুধু v2-এর সাথে মেলায়।

CI + RegistryCompatibility চেক
orders TopicRetention: চিরকাল
Analytics (নতুন)Offset 0 থেকে পড়ে
v3 বনাম v2 চেক: ✓
ধাপ 1 / 4
1

v3 Reader v2 Data পড়ছে: amount নেই, Default "0" বসে যায়। শেষ Version-এর সাথে Backward Compatible, তাই v3 Register হলো।

যে Data জমা থাকে আর আবার পড়া হয়, তার জন্য _TRANSITIVE মোড নাও। সাধারণ মোড শুধু সেখানে, যেখানে Data তাড়াতাড়ি পড়া হয়ে যায় আর আর কখনো Replay হয় না।

#কে আগে Deploy হবে

Compatibility মোড আসলে এক দিকের প্রতিশ্রুতি। Deploy-এর ক্রমও সেটা মেনে চলতে হবে, কারণ Registry চেক করে Schema, তোমার Deploy-এর ক্রম না:

মোডনিরাপদ ক্রমকেনউল্টো করলে কী ভাঙে
BACKWARDআগে Consumer, তারপর Producerনতুন Reader পুরোনো আর নতুন দুটোই পড়ে; পুরোনো Reader নতুন Data পড়বে, এমন কোনো কথা নেইProducer আগে গেলে এমন Data লেখে যা পুরোনো Consumer পড়তে পারে না: Deserialize Error, আটকে থাকা Consumer
FORWARDআগে Producer, তারপর Consumerপুরোনো Reader নতুন Data পড়ে; নতুন Reader পুরোনো Data পড়বে, এমন কথা নেইConsumer আগে গেলে, বা পুরোনো Data-য় ফিরে গেলে, সেটা পড়তে পারে না
FULLযে কেউদুই দিকই টেকেকিছুই না; এজন্যই ভাগ করে ব্যবহার করা Topic-এ FULL জনপ্রিয়

কে আগে Deploy হবে: Compatibility মোড ঠিক করে

v2 couponCode বাদ দেয়, অথচ v1 Reader Schema-র সেটা লাগে (Default নেই)। Registry BACKWARD মোডে, তাই v2 গ্রহণ করে: v2 Reader v1 Data পড়তে পারে। কিন্তু v1 Reader v2 Data পড়বে, এমন কোনো কথা নেই।

পরিস্থিতি

আগে Shipping Upgrade হয়, তারপর Producer।

Order svcProducerv1
orders TopicKafka
ShippingConsumerv2
Shipping v2 Deploy
ধাপ 1 / 6
1

BACKWARD-এ নতুন Reader-ই দুই আকার সামলানোর নিশ্চয়তা পায়, তাই সে আগে যায়।

দুটো ফল Engineer-রা ঠেকে শেখে:

  • Rollback-এর জন্য Forward Compatibility লাগে। ধরো Producer-এর পরিবর্তনটা শুধু Backward Compatible। Producer Ship হলো, Data লিখল, তারপর ফেরাতে হলো। পুরোনো Producer Code এখন নতুন Data-র Reader, আর BACKWARD সেটার কোনো প্রতিশ্রুতি দেয়নি। বিনা ঝামেলার Rollback চাইলে পরিবর্তনটা দুই দিকেই Compatible (FULL) হতে হবে, নয়তো দুই Release-এ Ship করো: আগে সব Reader-কে নতুন আকার শেখাও, তারপর সেই আকারে লেখা শুরু করো।
  • Producer ফেরালেও সে যা লিখেছে তা মুছে যায় না। খারাপ Record Topic-এই থেকে যায়। বেরোনোর পথ সাধারণত সামনে: এমন Reader Ship করো যে এগুলো সামলাতে পারে, অথবা জেনেবুঝে এড়িয়ে যাও (Dead-letter Queue আছে Chapter 30-এ)।

নিজেকে যাচাই করোপ্রশ্ন

তোমার Registry Subject FORWARD মোডে, আর একটা নতুন Schema Version এইমাত্র গৃহীত হলো। কোন Deploy-এর ক্রম নিরাপদ?

#নিয়মগুলো, Format ধরে ধরে

নিয়ম আলাদা কারণ Format-গুলো Field চেনে আলাদা ভাবে (Lesson ২.৯): Protobuf আর Thrift চেনে নম্বর দিয়ে, Avro চেনে নাম দিয়ে, হাতে Writer Schema রেখে, আর JSON চেনে নাম দিয়ে, কোনো Schema ছাড়াই।

#Protobuf: Field Number-এ কখনো হাত দিও না

Wire-এ প্রতিটা Protobuf Field হলো (field number, wire type, value); নাম কখনো যায় না। দুই পাশ নিজের নিজের কপির .proto Compile করে, তাই তারা একমত শুধু নম্বরে। নিয়মগুলো এখান থেকেই আসে:

  1. কোনো Field-এর নম্বর কখনো বদলাবে না, আর কোনো নম্বর আবার ব্যবহার করবে না। পুরোনো অর্থে লেখা Data নতুন অর্থে পড়া হবে। পুরো Message নষ্ট হয় না, কিন্তু যে Field-এ লাগে সেটা নিঃশব্দে ভুল পড়া হয় বা হারায়; কিছু ব্যর্থ হয় না বলে প্রায়ই সেটা আরও খারাপ।
  2. Field যোগ করা দুই দিকেই নিরাপদ। পুরোনো Reader অচেনা Field পার হয়ে যায় (Wire Type বলে দেয় কত Byte পার হতে হবে), আর Protobuf 3.5 থেকে proto3 অচেনা Field রেখেও দেয়, Message আবার Serialize করলে লিখেও দেয়। পুরোনো Data-য় Field না থাকলে নতুন Reader Default দেখে (শূন্য, খালি String, false)।
  3. Reserve করলে Field মোছা নিরাপদ। Field সরাও, আর তার নম্বর আর নাম দুটোই reserved করো, যাতে কেউ নতুন অর্থে ফিরিয়ে আনতে না পারে:
message OrderPlaced {
  reserved 3;                 // ছিল coupon_code, ২০২৬-০৩-এ সরানো
  reserved "coupon_code";
  string order_id = 1;
  int64  amount_paisa = 2;    // টাকা রাখো পূর্ণসংখ্যায় (পয়সা), কখনো double না
  string currency = 4;        // v2-তে যোগ; খালি মানে BDT
  optional string gift_note = 5;  // optional: “পাঠায়নি” আর “খালি” আলাদা
}
  1. নাম বদল Binary Wire-এ নিরাপদ (নাম তো যায় না), কিন্তু নাম ব্যবহার করে এমন সবকিছু ভাঙে: Protobuf-এর JSON Mapping, Text Format, Field Mask, আর Code-এর যে লাইনে পুরোনো নাম আছে। JSON Client থাকলে নাম বদলকে API পরিবর্তন হিসেবেই দেখো।
  2. Type বদল বেশিরভাগ ক্ষেত্রেই অনিরাপদ। কয়েকটা দলের Wire Encoding এক, সাবধানে অদলবদল চলে: int32, uint32, int64, uint64 আর bool (C-এর Cast-এর মতো মান ছেঁটে যেতে পারে); sint32 আর sint64; string আর bytes (bytes যদি বৈধ UTF-8 হয়); fixed32 আর sfixed32; fixed64 আর sfixed64। এর বাইরে, যেমন int64 থেকে string, Wire Type মেলে না: বেশিরভাগ Library তখন মানটাকে অচেনা Field ধরে, তাই সেটা নিঃশব্দে খালি পড়ে।
  3. Enum আসলে সংখ্যা। proto3-এ Enum খোলা: পুরোনো Reader নতুন মান (ধরো 4) পেলে নাম না জানলেও সংখ্যাটা রেখে দেয়। Wire টিকে যায়, কিন্তু default শাখা ছাড়া switch থাকা Code টেকে না। সবসময় অচেনা মান সামলাও, আর 0 মানটা ..._UNSPECIFIED রাখো, যাতে না-আসা মানকে কখনো আসল মান ভাবা না হয়।
  4. required আর নেই। proto2-এর required পরে যেকোনো মোছাকে Breaking বানিয়ে দিত, তাই proto3 এটা বাদ দিয়েছে (Protobuf Editions শুধু পুরোনো File-এর জন্য একটা Legacy সুবিধা হিসেবে রেখেছে)। থাকা-না-থাকা Code-এ যাচাই করো। “Set করা হয়নি” আর Default আলাদা করতে হলে proto3-এ optional দাও (3.15 থেকে চলে)।
  5. oneof আর একক Field-কে repeated করার ব্যাপারে সাবধান। পুরোনো Field oneof-এর ভেতরে বা বাইরে নেওয়া, বা সংখ্যা-ধরন (Cardinality) বদলানো কিছু ভাষায় Data হারায়; নতুন Field যোগ করাই ভালো।

Protobuf: Field Number-ই চুক্তি

Protobuf প্রতিটা Field পাঠায় (Field Number, Wire Type, মান) হিসেবে; নাম কখনো যায় না। Reader আর Writer নিজের নিজের কপির .proto Compile করে, তাই তারা একমত শুধু নম্বরে।

পরিস্থিতি

নতুন Writer string gift_note = 5 যোগ করে। Reader-এর কাছে এখনো পুরোনো .proto।

Writerorder.proto থেকে Compile করাv2
Byteনম্বর + Wire Type + মান
Readerনিজের কপির .protov1
1:"o-101" 2:150000 5:"happy eid"
ধাপ 1 / 4
1

প্রতিটা Field-এর আগে একটা Tag Byte, তাতে তার নম্বর আর Wire Type। Field 5-এর Wire Type বলে “দৈর্ঘ্যসহ”, তারপর দৈর্ঘ্য।

#Avro: Default আর নাম

Avro Byte-এ নাম নেই, নম্বরও নেই, শুধু Writer Schema-র ক্রমে মান। পড়তে Reader-এর লাগে হুবহু Writer Schema (Avro Container File-এর Header থেকে, বা Schema Registry থেকে), তারপর নিজের Reader Schema-র সাথে Field ধরে ধরে নাম মিলিয়ে Resolve করে:

  • Writer-এ আছে, Reader-এ নেই এমন Field: পার হয়ে যায়।
  • Reader-এ আছে, Writer-এ নেই এমন Field: Reader-এর Default বসে; Default না থাকলে পড়া ব্যর্থ।
  • দুটোতেই আছে এমন Field: Type মিলতে হবে, বা বৈধ Promotion হতে হবে: int → long, float, double; long → float, double; float → double; string ↔ bytes।

তাই Avro-র নিয়ম:

  1. যোগ করা প্রতিটা Field-এর Default চাই (Backward-এর জন্য: নতুন Reader, পুরোনো Data)।
  2. শুধু সেই Field মোছো, যার Default ছিল (Forward-এর জন্য: Field উধাও হলে পুরোনো Reader Default বসায়)।
  3. নাম বদলাও Reader Schema-র aliases দিয়ে। Alias নাম বদলকে শুধু Backward Compatible করে: পুরোনো Reader এখনো পুরোনো নাম খোঁজে। কোনো Writer-এর আগে সব Reader Upgrade করো।
  4. Optional মানে null-সহ Union, আর Default-কে Union-এর প্রথম Type-এর সাথে মিলতে হবে, তাই "null" আগে দাও: {"name": "note", "type": ["null", "string"], "default": null}।
  5. Enum: নতুন Symbol যোগ করলে পুরোনো Reader ভাঙে, যদি না তার Enum-এ একটা default Symbol থাকে (Avro 1.9 থেকে চলে)।

নোটের উদাহরণের Schema, একটা সংশোধনসহ:

{
  "type": "record",
  "name": "OrderCreated",
  "fields": [
    { "name": "orderId",  "type": "string" },
    { "name": "amount",   "type": { "type": "bytes", "logicalType": "decimal", "precision": 12, "scale": 2 } },
    { "name": "currency", "type": "string", "default": "BDT" }
  ]
}

currency-র Default-ই নতুন Reader-কে currency ছাড়া পুরোনো Event পড়তে দেয়। নোটে amount-এর Type ছিল double: টাকা কখনো Binary Floating Point-এ রেখো না (0.1 + 0.2 ঠিক 0.3 হয় না)। decimal Logical Type নাও, বা সবচেয়ে ছোট এককের (পয়সা, সেন্ট) পূর্ণসংখ্যা।

Avro: Reader, Writer-এর Schema-কে Field-এর নাম ধরে মেলায়

Avro Byte-এ নাম বা নম্বর কিছুই থাকে না, শুধু Writer Schema-র ক্রমে মান। Decode করতে Reader-এর লাগে হুবহু Writer Schema, তারপর নিজের Schema-র সাথে নাম ধরে Field মেলায়।

পরিস্থিতি

Writer v1 {orderId, amount}; Reader v2 Default BDT সহ currency যোগ করে (নোটের Schema)।

Writer SchemaEncode করতে লেগেছেv1
Avro Byteশুধু মান, ক্রমমতো
Reader SchemaCode যা চায়v2
"o-101" | 150000
ধাপ 1 / 3
1

Writer পরপর দুটো মান লেখে। Byte-এর কোথাও লেখা নেই কোনটা কোন Field।

#JSON: Tolerant Reader আর দুর্বল Schema

JSON-এর Wire-এ কোনো Schema নেই, তাই চুক্তি হলো Field-এর নাম আর তাদের মানের Type, আর Compatibility পুরোটাই নির্ভর করে Reader কীভাবে লেখা তার ওপর। নিয়মটার নাম Tolerant Reader: শুধু দরকারি Field পড়ো, বাকি সব উপেক্ষা করো, আর না-থাকা Optional Field-কে Default ধরো।

Libraryঅচেনা Field ডিফল্টেমন্তব্য
Java Jackson ObjectMapperব্যর্থ হয় (FAIL_ON_UNKNOWN_PROPERTIES true)Spring Boot-এর নিজে সাজানো Mapper এটা বন্ধ রাখে; খালি Jackson রাখে না
Go encoding/jsonউপেক্ষা করেDecoder.DisallowUnknownFields() দিলে কড়া হয়
Python Pydantic v2উপেক্ষা করে (extra='ignore')extra='forbid' দিলে কড়া হয়
TypeScript JSON.parse + একটা Typeউপেক্ষা করে (চলার সময় কোনো চেকই নেই)Type যা-ই বলুক, না-থাকা Field চলার সময় undefined

JSON-এর চেনা ফাঁদ: নাম বদল (amount → total) সব Reader-এর কাছে মোছার মতো দেখায়; সংখ্যা String হয়ে গেলে (নোটের "amount": "FIFTEEN HUNDRED") Typed Reader ভাঙে; null, না-থাকা Key আর খালি String একেক ভাষায় একেক মানে; আর ৬৪ bit ID JavaScript-এর সংখ্যায় নির্ভুলতা হারায় (Lesson ২.৯), এজন্যই অনেক API প্রথম দিন থেকেই ID পাঠায় String হিসেবে।

JSON Schema JSON-কে বর্ণনা আর যাচাই করে, কিন্তু এটা বানানো হয়েছিল যাচাইয়ের জন্য, Evolution-এর জন্য না; তাই এর Registry Compatibility চেক দুর্বল, আর সহজেই ভুল বোঝা যায়। আসল সুইচ হলো additionalProperties:

  • বন্ধ ("additionalProperties": false): নতুন যেকোনো Field পুরোনো Reader-এর যাচাইয়ে ব্যর্থ, তাই প্রতিটা যোগই Forward Incompatible।
  • খোলা (ডিফল্ট): যোগ পাস করে, কিন্তু নাম বদলও “পাস” করে, কারণ পুরোনো নামটা স্রেফ একটা অনুমোদিত বাড়তি Property হয়ে যায়। একটা জরুরি অর্থ যে উধাও হলো, Checker সেটা দেখতে পায় না।

অনেক Team Public API-তে JSON রাখে (পড়া সহজ, সব জায়গায় চলে), আর ভেতরের Event-এ Registry-সহ Avro বা Protobuf নেয়, যেখানে কড়া নিয়মের দাম উঠে আসে।

#Thrift

Thrift Field চেনে সংখ্যার ID দিয়ে, Protobuf-এর মতোই, তাই নিয়মও একই: ID কখনো বদলাবে না বা আবার ব্যবহার করবে না, নতুন Field যোগ করো নতুন ID-তে optional হিসেবে, আর অবসরে যাওয়া ID-গুলোর তালিকা Comment-এ রেখে দাও যাতে কেউ আবার না নেয়। পুরোনো Reader অচেনা Field পার হয়ে যায় (আর সাধারণত রেখে দেয় না)। required এড়িয়ে চলো: Thrift-ব্যবহারকারীদের নিয়মই হলো “required is forever”, কারণ পরে সরালে প্রতিটা পুরোনো Reader ভাঙে।

#পরিবর্তন ধরে ধরে এক নজরে

পরিবর্তনProtobufAvroJSON (Tolerant Reader)
Optional Field যোগ✅ দুই দিকেই✅ দুই দিকেই, যদি Default থাকে✅ দুই দিকেই (কড়া Reader-এ ❌)
Default ছাড়া Field যোগপ্রযোজ্য না (proto3-এর সব Field-এর Default আছে)❌ Backward (পুরোনো Data-য় নতুন Reader ব্যর্থ)⚠️ না থাকলেও Reader-কে সামলাতে হবে
Field মোছা✅ যদি নম্বর আর নাম reserved হয়✅ Backward; Forward শুধু Default থাকলে❌ যে Reader-এর এখনো লাগে তার জন্য
Field-এর নাম বদল✅ Binary, ❌ JSON Mapping আর Code⚠️ শুধু Backward, aliases দিয়ে❌ (একটা মোছা আর একটা যোগ)
Type বদল❌ Compatible দল ছাড়া❌ Promotion ছাড়া (int → long…)❌
Field Number বা নাম আবার ব্যবহার❌ নিঃশব্দ গোলমাল❌ পুরোনো Data নতুন অর্থ পায়❌ পুরোনো Data নতুন অর্থ পায়
Enum-এ নতুন মান⚠️ Wire ঠিক, Code-কে অচেনা সামলাতে হবে❌ Enum default ছাড়া পুরোনো Reader-এ⚠️ Code-কে অচেনা String সামলাতে হবে

#Schema Registry, ওপর থেকে এক নজরে

যখন কয়েক ডজন Service একই Event ভাগ করে, তখন কাউকে Schema-গুলো ধরে রাখতে হয় আর নিয়ম চাপাতে হয়। Schema Registry হলো একটা কেন্দ্রীয় Service, যেটা প্রতিটা Schema-র প্রতিটা Version জমা রাখে, প্রতিটাকে একটা ID দেয়, আর প্রতিটা নতুন Version-কে Subject-এর Compatibility মোডের সাথে মিলিয়ে দেখে। Kafka-র জগতে এর গুরুত্ব সবচেয়ে বেশি। এখানে শুধু কাঠামোটা দেখব; পুরো Registry আছে Chapter 32-এ।

নোটের উদাহরণটা দিয়ে শুরু করি: Order Service পাঠায় {"orderId": "ORD-101", "amount": 1500}, আর Inventory Service সেটা নেয়। "currency": "BDT" যোগ করলে Inventory-র ক্ষতি হতেও পারে, না-ও পারে, সেটা নির্ভর করে সে কীভাবে পড়ে তার ওপর; আর {"orderId": 101, "amount": "FIFTEEN HUNDRED"} করলে দুটো Type-ই বদলায়, পুরোনো Consumer ভাঙবেই। Registry এই “হতেও পারে, না-ও পারে”-কে একটা চেকে বদলে দেয়, যেটা পরিবর্তন Ship হওয়ার আগেই চলে।

এটা কীভাবে কাজ করে (Confluent Schema Registry, সবচেয়ে প্রচলিতটা):

  1. CI-তে Register। Pipeline একটা Subject-এর জন্য নতুন Schema জমা দেয় (ডিফল্টে নাম Topic থেকে, যেমন orders-value)। Registry সেটাকে Subject-এর মোড (ডিফল্ট BACKWARD) দিয়ে মেলায়, তারপর হয় একটা ID দেয়, নয়তো 409 Conflict দিয়ে ফিরিয়ে দেয়।
  2. Produce। Producer-এর Serializer Schema-র ID একবার খুঁজে নিয়ে Cache করে। প্রতিটা Record লেখা হয় এভাবে: Magic Byte 0 + ৪ Byte-এর Schema ID + Encode করা মূল অংশ (Protobuf Record-এ ID-র পরে Message Index-ও থাকে)। প্রতিটা Message-এ পুরো Schema-র বদলে মাত্র ৫ Byte।
  3. Consume। Consumer-এর Deserializer ID পড়ে, সেই Schema একবার এনে (GET /schemas/ids/{id}) Cache করে, তারপর Writer Schema দিয়ে Decode করে নিজের Reader Schema-য় মেলায়।

Data কখনো Registry দিয়ে যায় না। এটা পাশে বসে থাকা একটা খোঁজার Service, Client কোনো অচেনা (Cache-এ নেই এমন) Schema পেলে তখনই কাজে লাগে। নোটের Diagram-এ Registry-কে Producer আর Topic-এর মাঝখানে বসানো হয়েছে; ওপরেরটাই শুদ্ধ ছবি।

Schema Registry: Schema আলাদা পথে, প্রতিটা Record-এ ৫ Byte-এর Prefix

Confluent-ধাঁচের প্রবাহের সারসংক্ষেপ; Chapter 32-এ আরও গভীরে যাব। Record কখনো Registry দিয়ে যায় না: Client সেখান থেকে Schema এনে Cache করে রাখে।

পরিস্থিতি

Schema v2 দিয়ে প্রথম Record, তারপর স্বাভাবিক অবস্থা।

ProducerSerializer
Schema Registrysubject orders-value
Kafka Topicorders
ConsumerDeserializer
orders-value-এ Schema v2 খোঁজো
ধাপ 1 / 6
1

Serializer Registry-কে তার Schema-র ID জিজ্ঞেস করে, Subject ধরে (ডিফল্টে “topic-value”)। Production-এ Schema আগেই CI Register করে রেখেছে, App না।

Registry কী দেয়: Producer আর Consumer-এর মাঝে একটাই চুক্তি, কম Type অমিল, Version-এর ইতিহাস, নিরাপদ Deploy, আর আগেভাগে ধরা পড়া Breaking পরিবর্তন। কী নেয়: আরও একটা সবচেয়ে জরুরি Service (এটা বন্ধ থাকলে নতুন Pod চালু হতে পারে না), এমন ID যার মানে শুধু একটা Registry-র ভেতরে (Cluster-এর মধ্যে Topic কপি করলে Schema-ও কপি করতে হয়), আর শৃঙ্খলা: Production-এ Auto-registration বন্ধ রাখো (auto.register.schemas=false), যাতে শুধু Review হওয়া Pipeline-ই Schema যোগ করতে পারে।

অন্য Registry-গুলোও একই ধারণায়, নিজের নিজের খুঁটিনাটি নিয়ে: Apicurio Registry (Confluent-এর API-র সাথে মেলে), AWS Glue Schema Registry (BACKWARD_ALL-এর মতো মোড, আর ৫ Byte-এর Prefix-এর বদলে Schema Version-এর UUID-সহ নিজস্ব Header), আর Protobuf Module-এর জন্য Buf Schema Registry।

#Event-এর Version আর Upcasting

Event Sourcing প্রতিটা পরিবর্তনকে একটা অপরিবর্তনীয় Event হিসেবে জমা রাখে, আর সেগুলো আবার চালিয়ে State বানায় (Chapter 32)। তাই এখানে Evolution সবচেয়ে কড়া: Event-ই আসল সত্য, কখনো Edit হয় না, আর একটা Replay বছর পরে এ যাবৎ লেখা প্রতিটা Version পড়ে। সাধারণ হাতিয়ার:

  • আগে Compatible Evolution। Default-সহ Field যোগ করো; কোনো Field-এর অর্থ কখনো বদলাবে না। বেশিরভাগ পরিবর্তনে এর বেশি কিছু লাগার কথা না।
  • Event-এর ভেতরেই Version: একটা স্পষ্ট schemaVersion (বা Type হিসেবে OrderPlaced.v2), যাতে Reader জানে হাতে কী আছে।
  • Upcasting: আকার বদলাতেই হলে ছোট্ট একটা Pure Function লেখো, যেটা পড়ার সময় v1 Event-কে v2 Event বানায়। Handler শুধু সর্বশেষ Version দেখে; জমা Event-এ কেউ হাত দেয় না। Upcaster শিকলের মতো জোড়া যায় (v1 → v2 → v3)।
  • Copy and Transform: কদাচিৎ, প্রতিটা Event বদলে নিয়ে নতুন একটা Stream লেখো, তারপর সেটায় সরে যাও। খরচ বেশি, তবে পুরোনো Upcaster ফেলে দেওয়া যায়।
  • Replay-এর Test: প্রতিটা পুরোনো Event Version-এর একটা নমুনা রেখে CI-তে Replay করো। হারানো Upcaster ধরা পড়ে ঠিক সেদিন, যেদিন তুমি একটা Projection আবার বানাচ্ছ বা দুর্যোগ থেকে ফিরছ।

Event Sourcing: পড়ার সময় পুরোনো Event-কে Upcast করা

Event-sourced System-এ Event-ই আসল সত্য, আর সেগুলো কখনো Edit হয় না। Order Event-এর আকার বদলালেও পুরোনো Event আবার চালাতে পারতে হবে। এখানে সারসংক্ষেপ; Event Sourcing আছে Chapter 32-এ।

পরিস্থিতি

v1 amount রাখত 1500.0 টাকা হিসেবে; v2 রাখে পূর্ণসংখ্যায় amountPaisa, সাথে currency।

Event Storeঅপরিবর্তনীয়, চিরকাল
Upcasterপড়ার সময় v1 → v2
Handlerশুধু v2 চেনে
order event v1 {amount: 1500.0}
ধাপ 1 / 4
1

একটা Projection প্রথম Event থেকে আবার বানানো হচ্ছে। সবচেয়ে পুরোনো Event-গুলো v1 আকারে।

#API Versioning: সীমানাতেও একই নিয়ম

REST বা gRPC API-ও একটা Schema, যার Reader অন্যদের Code, তাই একই যুক্তি খাটে (API Design আর Versioning-এর গভীরে যাব Chapter 26-এ):

  • যোগ করা পরিবর্তনে নতুন Version লাগে না: নতুন Optional Request Field, নতুন Response Field, নতুন Endpoint। তবে এটা চলে শুধু Client Tolerant Reader হলে, আর সেটা API Documentation-এ লিখে রাখা ভালো।
  • Breaking পরিবর্তন পায় নতুন Version: একটা Path (/v2/orders), একটা Header বা একটা Media Type। কিছু Provider তারিখ দিয়ে Version করে, আর Server-এ পুরোনো Request-কে নতুন আকারে বদলে নেয়; এটা আসলে API-র Upcasting।
  • পুরোনো Version অবসরে পাঠাও জোরে ঘোষণা দিয়ে: একটা তারিখ জানাও, Deprecation Header (RFC 9745) আর Sunset Header (RFC 8594) পাঠাও, মেপে দেখো এখনো কারা পুরোনো Version ডাকে, তারপরই সরাও।
  • Mobile Client হলো লম্বা লেজ: আজ Install হওয়া একটা App Version কয়েক বছর তোমার API ডাকতে পারে, তাই Public Response Format-এ FULL-এর মতো যত্ন লাগে।

#Database Schema: একই বুদ্ধি, Expand আর Contract

Database Table-ও একটা Schema, আর Rolling Deploy মানে পুরোনো আর নতুন Application Code একই সময়ে সেটায় Query চালায়। নিরাপদ ধাঁচ হলো Expand → Migrate → Contract, যেটা নাম বদলের Scenario-তে আগেই দেখেছ:

  1. Expand: নতুন Column যোগ করো (Nullable বা Default-সহ)। পুরোনো Code এটা উপেক্ষা করে।
  2. Migrate: এমন Code Deploy করো যা দুটো Column-এই লেখে, পুরোনো Row-গুলো Batch-এ Backfill করো, তারপর Read-গুলো নতুন Column-এ সরাও।
  3. Contract: চালু কোনো Code আর পুরোনো Column ব্যবহার না করলে সেটা Drop করো।

প্রতিটা ধাপ পাশের চালু Code-এর সাথে Backward আর Forward দুই দিকেই Compatible। Downtime ছাড়া Migration-এর বিস্তারিত আছে Chapter 46-এ।

#Production-এর আগেই ভাঙা পরিবর্তন ধরা

যে নিয়ম কেউ চেক করে না, সেটা নিছক পরামর্শ। এগুলো CI-তে বসাও:

  • Schema Compatibility চেক: Merge করার আগে Registry-কে জিজ্ঞেস করো (POST /compatibility/subjects/{subject}/versions/latest), বা একই কাজ করা Build Plugin ব্যবহার করো।
  • Breaking-change Linter: buf breaking .proto File-গুলোকে Main Branch-এর সাথে মেলায়, আর আবার ব্যবহার করা নম্বর, reserved ছাড়া মোছা Field, Type বদল আর আরও অনেক কিছুতে ব্যর্থ হয় (শুধু Wire, Wire আর JSON, বা পুরো Source Compatibility: আলাদা আলাদা নিয়মের সেট)।
  • Golden নমুনা: প্রতিটা পুরোনো Version-এর Encode করা উদাহরণ রেখে দাও, আর যাচাই করো নতুন Code সেগুলো এখনো পড়তে পারে।
  • Consumer-driven Contract Test (সবচেয়ে পরিচিত Tool হলো Pact): প্রতিটা Consumer জানায় সে আসলে কী ব্যবহার করে, Provider-এর Build তার আসল Message-কে প্রতিটা Consumer-এর চুক্তির সাথে যাচাই করে, আর can-i-deploy চেক এমন Release আটকে দেয় যা Production-এ চলা কোনো Consumer-কে ভাঙবে। Schema চেক Type জানে; Contract Test জানে প্রতিটা Consumer কীসের ওপর নির্ভর করে।

Consumer-driven Contract Test: Deploy-এর আগেই ভাঙা পরিবর্তন ধরো

Schema চেক Type জানে; Contract Test জানে প্রতিটা Consumer আসলে কী ব্যবহার করে। দুটোই CI-তে চলে।

পরিস্থিতি

Order Event থেকে Billing-এর কী লাগে, সে লিখে রাখে।

Billing CIConsumer
Contract BrokerPact Broker
Order svc CIProvider
চুক্তি: orderId, amount (number) লাগবে
ধাপ 1 / 3
1

Billing-এর নিজের Test থেকে একটা চুক্তি তৈরি হয়: সে যে Field আর Type পড়ে, তার বেশি কিছু না। Billing-এর Version-সহ এটা প্রকাশ হয়।

#System Design-এ Schema Evolution

মোড বেছে নেওয়া:

Dataযা নেবেকেন
অনেক Team-এর পড়া, Replay হওয়া বা Compacted Kafka TopicFULL_TRANSITIVE (বা BACKWARD_TRANSITIVE)যেকোনো Deploy-ক্রম, যেকোনো বয়সের যেকোনো Reader
কয়েক মিনিটে পড়া হয়ে যাওয়া, কখনো Replay না হওয়া QueueBACKWARD বা FULLশুধু সাম্প্রতিক Version-ই পড়া হয়
Event Store (Event Sourcing)Transitive আর Upcasterপ্রতিটা Version চিরকাল Replay হয়
Service-এর মধ্যে ভেতরের gRPCProtobuf-এর নিয়ম আর buf breakingকিছু জমা থাকে না; Rolling Deploy-এ দুই দিকই লাগে
Public REST APIশুধু যোগ, ভাঙলে নতুন Version, Deprecation HeaderClient তোমার হাতে নেই
Development TopicNONEগতি, তবে ভাগ করা Data-য় কখনোই না

যে লক্ষণগুলো চিনে রাখা দরকার:

লক্ষণসম্ভাব্য কারণ
Consumer একটা Offset-এ আটকে, Deserialize Error, Lag বাড়ছেProducer এমন পরিবর্তন Deploy করেছে যা তার পুরোনো Consumer পড়তে পারে না (Deploy-এর ভুল ক্রম)
একটা Field হঠাৎ খালি বা শূন্য, কোনো Error নেইনরম Parser একটা নাম বদল পড়েছে, বা Protobuf-এ Wire Type অমিল
পুরোনো Record-এর একটা Field-এ আজগুবি মানField Number বা নাম নতুন অর্থে আবার ব্যবহার হয়েছে
নতুন Consumer বা Replay ব্যর্থ, চালু Consumer ঠিকNon-transitive Compatibility; Topic-এ পুরোনো Data
নতুন Pod চালু হয় না, পুরোনোগুলো ঠিকSchema Registry-তে পৌঁছানো যাচ্ছে না, Cache ঠান্ডা
Rollback-এর পরে Data থেকে একটা Field হারিয়ে গেছেপুরোনো Code অচেনা Field না রেখেই Record আবার লিখেছে

Interview-এর প্রশ্ন: “বারোটা Team ব্যবহার করে এমন একটা Event-এ Field-এর নাম কীভাবে বদলাবে?” (Expand–migrate–contract, কখনো এক ধাপে না)। “Backward আর Forward Compatibility-র পার্থক্য কী, আর কে আগে Deploy হয়?” “Protobuf-এ reserved কেন আছে?” “Event-sourced System-এ Event কীভাবে বদলাবে?” (Compatible পরিবর্তন, Version, Upcaster)। “প্রতিটা Kafka Record-এ Schema ID কেন থাকে, আর Registry বন্ধ হলে কী হয়?” ভালো উত্তর সবসময় বলে দেয় কে পড়ে, কে লেখে, আর কোন ক্রমে তারা Deploy হয়।

#নিজে করে দেখো

Buf দিয়ে একটা Protobuf পরিবর্তন Main Branch-এর সাথে মিলিয়ে দেখো (আবার ব্যবহার করা নম্বর বা reserved না করে মোছা Field থাকলে ব্যর্থ হবে):

buf breaking --against '.git#branch=main'
# output similar to:
# order.proto:12:3:Field "3" on message "OrderPlaced" changed name from "coupon_code" to "shipping_note".

একটা Schema Register না করেই Registry-কে জিজ্ঞেস করো সেটা গৃহীত হবে কি না (Confluent API, Local Test Registry):

curl -s -X POST -H 'Content-Type: application/vnd.schemaregistry.v1+json' \
  --data '{"schema": "{\"type\":\"record\",\"name\":\"OrderCreated\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"string\"}]}"}' \
  'http://localhost:8081/compatibility/subjects/orders-value/versions/latest?verbose=true'
# {"is_compatible":false,"messages":["... কারণ, amount Field আর দুটো Type-এর নামসহ ..."]}

curl -s http://localhost:8081/config/orders-value                     # Subject-এর মোড
curl -s -X PUT -H 'Content-Type: application/vnd.schemaregistry.v1+json' \
  --data '{"compatibility": "FULL_TRANSITIVE"}' http://localhost:8081/config/orders-value

Python-এ fastavro দিয়ে Avro-র Schema Resolution নিজের চোখে দেখো: v1 দিয়ে লেখো, v2 দিয়ে পড়ো।

import io
from fastavro import parse_schema, schemaless_writer, schemaless_reader

v1 = parse_schema({"type": "record", "name": "Order", "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "amount", "type": "long"}]})
v2 = parse_schema({"type": "record", "name": "Order", "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "amount", "type": "long"},
    {"name": "currency", "type": "string", "default": "BDT"}]})

buf = io.BytesIO()
schemaless_writer(buf, v1, {"orderId": "o-101", "amount": 150000})
buf.seek(0)
print(schemaless_reader(buf, v1, v2))
# {'orderId': 'o-101', 'amount': 150000, 'currency': 'BDT'}   Default বসে গেছে

এবার v2 থেকে "default": "BDT" সরিয়ে আবার চালাও: পড়া ব্যর্থ হবে, কারণ নতুন Reader-এর হাতে currency-তে বসানোর মতো কিছু নেই। Registry ঠিক এই Backward চেকটাই তোমার হয়ে চালাত।

#সারসংক্ষেপ

মূল শিক্ষা

  • Schema Evolution = এমন অবস্থায় Data Format বদলানো, যখন পুরোনো আর নতুন Code, পুরোনো আর নতুন Data একসাথে আছে। Rolling Deploy, আলাদা Team, Rollback, জমা Data আর পুরোনো Client-এর কারণে এটাই স্বাভাবিক অবস্থা, ব্যতিক্রম না।
  • Backward: নতুন Reader পুরোনো Data পড়ে (আগে Consumer Deploy)। Forward: পুরোনো Reader নতুন Data পড়ে (আগে Producer Deploy)। Full: দুটোই (যেকোনো ক্রম)। Transitive: আগের প্রতিটা Version-এর সাথে; Replay বা জমা থাকা সবকিছুর জন্য লাগে।
  • Default-সহ যোগ করা পরিবর্তনই নিরাপদ পথ। নাম বদল মানে একটা মোছা আর একটা যোগ; করো Expand → Migrate → Contract দিয়ে। Type বদল মানে ভাঙা; বদলে নতুন Field যোগ করো।
  • Protobuf: Field Number কখনো বদলাবে না বা আবার ব্যবহার করবে না, মোছা নম্বর আর নাম reserved, Field যোগ নিরাপদ, অচেনা Field রক্ষা পায়, অল্প কিছু Type বদলই নিরাপদ, অচেনা Enum মান সামলাও, required নেই।
  • Avro: Field মেলে নাম ধরে; যোগ করা Field-এর Default চাই, মোছা Field-এর Default থাকা উচিত ছিল; নাম বদল Alias দিয়ে (শুধু Backward); Optional = null আগে রেখে Union; শুধু তালিকার Type Promotion।
  • JSON: Tolerant Reader হও; কড়া Deserializer আর বন্ধ JSON Schema-র দিকে চোখ রাখো; টাকা কখনো Float-এ না।
  • Schema Registry প্রতিটা নতুন Version CI-তে চেক করে, আর প্রতিটা Record-এ বসায় Magic Byte + ৪ Byte-এর Schema ID; Data কখনো এর ভেতর দিয়ে যায় না। Event Store যোগ করে Version আর Upcaster; API যোগ করে Version আর Deprecation Header; Database চলে Expand আর Contract-এ; শুধু Type যা ধরতে পারে না, Contract Test তা ধরে।