7 মিনিট

বাইরের বনাম ভেতরের ডেটা — অ্যাপের জন্য Pat Helland-এর পাঠ

Pat Helland-এর 'বাইরের বনাম ভেতরের' ডেটা ধারণা শিখুন—স্পষ্ট সীমানা, আইডেম্পোটেন্ট কল ডিজাইন করা এবং নেটওয়ার্ক ব্যর্থ হলে স্টেট মিলিয়ে নেওয়ার কৌশল।

বাইরের বনাম ভেতরের ডেটা — অ্যাপের জন্য Pat Helland-এর পাঠ

"বাইরে বনাম ভিতরে" সাধারণ ভাষায় কী বোঝায়

যখন আপনি একটি অ্যাপ তৈরি করেন, সহজে ভাবা যায় যে অনুরোধগুলো সুন্দরভাবে, একটির পর একটা সঠিক ক্রমে আসবে। বাস্তব নেটওয়ার্কগুলো ওইভাবে আচরণ করে না। ব্যবহারকারী স্ক্রিন freeze হওয়ায় “Pay” দুবার ট্যাপ করতে পারে। একটি মোবাইল সংযোগ বোতাম চাপার ঠিক পরেই বিচ্ছিন্ন হয়ে যেতে পারে। একটি webhook দেরিতে আসতে পারে, বা দুবার আসতে পারে। কখনও কখনও সেটা একেবারেই আসে না।

Pat Helland-এর বাইরে বনাম ভিতরে ডেটা ধারণা এই বিশৃঙ্খলতার জন্য একটি পরিষ্কার চিন্তার রীতি দেয়।

"বাইরে" কেমন দেখায়

"বাইরে" হল সেই সব কিছু যা আপনার সিস্টেম নিয়ন্ত্রণ করে না। এটি হলো যেখানে আপনি অন্য মানুষ ও সিস্টেমের সঙ্গে কথা বলেন, এবং যেখানে ডেলিভারির নির্ভরযোগ্যতা অনিশ্চিত: ব্রাউজার ও মোবাইল অ্যাপ থেকে HTTP রিকুয়েস্ট, কিউ থেকে মেসেজ, তৃতীয়‑পক্ষ webhook (পেমেন্ট, ইমেল, শিপিং), এবং ক্লায়েন্ট, প্রক্সি বা ব্যাকগ্রাউন্ড জব দ্বারা ট্রিগার হওয়া রিট্রাই।

বাইরে, ধরে নিন বার্তাগুলো বিলম্বিত হতে পারে, সদৃশ হতে পারে বা ভুল ক্রমে আসতে পারে। এমনকি যদি কিছু "সাধারণত নির্ভরযোগ্য" হয়, তবুও সেই দিনের জন্য ডিজাইন করুন যখন তা নির্ভরযোগ্য থাকবেনা।

"ভিতরে" মানে কী

"ভিতরে" হল যা আপনার সিস্টেমকে আপনি নির্ভরযোগ্য করতে পারেন। এটি সেই টেকসই স্টেট যা আপনি সংরক্ষণ করেন, আপনি যেসব নিয়ম প্রয়োগ করেন, এবং যেসব তথ্য পরে আপনি প্রমাণ করতে পারেন:

  • ডাটাবেস রেকর্ড এবং তাদের ইতিহাস
  • ব্যবসায়িক নিয়ম (উদাহরণ: “একটি অর্ডার শুধুমাত্র একবার পেইড হতে পারে”)
  • স্টেটের জন্য একটি সুত্র (pending, paid, canceled)

ভিতরে আপনি ইনভারিয়েন্টগুলো রক্ষা করেন। যদি আপনি "একটি অর্ডারের জন্য একবার পেমেন্ট" প্রতিশ্রুতি দেন, সেই প্রতিশ্রুতিটি ভিতরেই জোরদার করতে হবে, কারণ বাইরেকে বিশ্বাস করা যায় না।

মনোভাবের সরল পরিবর্তন হলো: নিখুঁত ডেলিভারি বা নিখুঁত টাইমিং ধরে নেবেন না। প্রতিটি বাইরের ইন্টারঅ্যাকশনকে একটি অনিশ্চিত পরামর্শ হিসেবে বিবেচনা করুন যা পুনরাবৃত্ত হতে পারে, এবং ভিতরকে নিরাপদভাবে সাড়া দিতে ডিজাইন করুন।

এটি ছোট টিম এবং সরল অ্যাপের ক্ষেত্রেও গুরুত্বপূর্ণ। প্রথমবার একটি নেটওয়ার্ক ত্রুটি ডুপ্লিকেট চার্জ বা আটকে থাকা অর্ডার তৈরি করলে সেটি তাত্ত্বিক না থেকে রিফান্ড, সাপোর্ট টিকিট এবং বিশ্বাসের ক্ষতিতে পরিণত হয়।

একটি বাস্তব উদাহরণ: ব্যবহারকারী "Place order" ট্যাপ করে, অ্যাপ একটি রিকুয়েস্ট পাঠায়, এবং সংযোগ বিচ্ছিন্ন হয়ে যায়। ব্যবহারকারী আবার চেষ্টা করে। যদি আপনার ভিতরে সেই চেষ্টা "একই চেষ্টা" চিহ্নিত করার উপায় না থাকে, আপনি দুটি অর্ডার তৈরি করতে পারেন, ইনভেন্টরি দ্বিগুণ রিজার্ভ করতে পারেন, বা দুবার কনফার্মেশন ইমেল পাঠাতে পারেন।

Pat Helland-এর মূল পাঠ

Helland-এর দৃষ্টান্ত সোজা: বাইরের বিশ্ব অনিশ্চিত, কিন্তু আপনার সিস্টেমের ভিতরকে ধারাবাহিক রাখতে হবে। নেটওয়ার্ক প্যাকেট হারায়, ফোন সিগন্যাল নষ্ট হয়, ক্লক ডিফট করে, এবং ব্যবহারকারী refresh ট্যাপে দেয়। আপনার অ্যাপ এসব কিছু নিয়ন্ত্রণ করতে পারে না। যা এটি নিয়ন্ত্রণ করতে পারে সেটা হলো স্পষ্ট সীমানা পেরিয়ে যখন ডেটা "সত্য" হিসেবে গ্রহণ করা হবে—সেই সিদ্ধান্ত।

এক সাধারণ মুহূর্তে সময় ও অনিশ্চয়তা

ভাবুন কেউ খারাপ Wi‑Fi থাকা একটি ভবনে হাঁটতে হাঁটতে ফোনে কফি অর্ডার করছে। তারা "Pay" ট্যাপ করে। স্পিনার চলে। নেটওয়ার্ক কেটে যায়। তারা আবার ট্যাপ করে।

হয়তো প্রথম অনুরোধ আপনার সার্ভারে পৌঁছেছে, কিন্তু রেসপন্স ফিরতে পারেনি। অথবা হয়তো কোনো অনুরোধই পৌঁছায়নি। ব্যবহারকারীর দৃষ্টিকোণ থেকে উভয় সম্ভাবনাই একই দেখায়।

এটাই সময় এবং অনিশ্চয়তা: আপনি এখনও জানেন না কী হয়েছে, এবং পরে জানতে পারেন। আপনার সিস্টেমকে অপেক্ষাকালীনও সংবেদনশীলভাবে আচরণ করতে হবে।

রিট্রাই, ডুপ্লিকেট, এবং পুনঃক্রমবিন্যাস

একবার আপনি গ্রহণ করেন যে বাইরে অনির্ভরযোগ্য, কয়েকটি "অদ্ভুত" আচরণ স্বাভাবিক হয়ে ওঠে:

  • রিট্রাইগুলি সদৃশ তৈরি করে (দুটি “Pay” অনুরোধ)।
  • বার্তাগুলো ভুল ক্রমে আসে (একটি "cancel" আসে "pay" এর আগে)।
  • একটি অনুরোধ প্রসেস হয়, কিন্তু ক্লায়েন্ট রেসপন্স দেখেনা।

বাইরের ডেটা একটি দাবি; বাস্তবতা নয়। "আমি পেমেন্ট করেছি" কেবল একটি বিবৃতি যা অনির্ভরযোগ্য চ্যানেলে পাঠানো হয়েছে। এটি একটি বাস্তবতা হয় কেবলমাত্র আপনি তা ভিতরে টেকসই ও সঙ্গতিপূর্ণভাবে রেকর্ড করলে।

এটি আপনাকে তিনটি ব্যবহারিক অভ্যাসের দিকে ঠেলবে: স্পষ্ট সীমানা নির্ধারণ, রিট্রাইকেও নিরাপদ রাখতে আইডেম্পোটেন্সি, এবং বাস্তবতা মিলেনি এমন পরিস্থিতির জন্য রিকনসিলিয়েশন পরিকল্পনা।

স্পষ্ট সীমানা: আপনার সিস্টেম কি দায়িত্ব নেয় এবং কি নেয় না

"বাইরে বনাম ভিতরে" ধারণাটি একটি ব্যবহারিক প্রশ্ন দিয়ে শুরু করে: আপনার সিস্টেমের সত্য কোথায় শুরু এবং শেষ হয়?

সীমার ভেতরে আপনি শক্তিশালী গ্যারান্টি দিতে পারেন কারণ আপনি ডেটা ও নিয়মগুলো নিয়ন্ত্রণ করেন। সীমার বাইরে আপনি সেরা প্রয়াস করেন এবং ধরে নেবেন বার্তা হারিয়ে যেতে পারে, সদৃশ হতে পারে, বিলম্বিত হতে পারে বা ভুল ক্রমে আসতে পারে।

বাস্তব অ্যাপে সেই সীমা প্রায়ই নিম্ন জায়গাগুলোতে দেখা যায়:

  • একটি API endpoint যা আপনার ডাটাবেসে একটি রেকর্ড লিখে
  • একটি queue consumer যা একটি ইভেন্টকে সংরক্ষিত পরিবর্তনে পরিণত করে
  • একটি callback হ্যান্ডলার যা প্রদানকারী কী ঘটেছে তা রেকর্ড করে
  • একটি sender যা আপনার নিজের স্টেট কমিট করার পরে অন্য সিস্টেমকে নোটিফাই করে

একবার আপনি সেই লাইন অঙ্কন করলে, ঠিক করুন কোন ইনভারিয়েন্টগুলো অমোঘ ভিতরে। উদাহরণ:

  • একটি অর্ডার ID আপনার ডাটাবেসে অনন্য।
  • ব্যালেন্স কখনো নেগেটিভ হবে না।
  • একটি স্টেট শুধু সামনে বাড়ে (created -> paid -> shipped)।
  • আপনি যে কোনো বাহ্যিক অনুরোধ গ্রহণ করেন তার একটি সংরক্ষিত অডিট ট্রেইল আছে।

সীমাটির জন্য "আমরা কোথায় আছি" পরিষ্কার ভাষা দরকার। অনেক ব্যর্থতা থাকে “আমরা আপনাকে শুনেছি” এবং “আমরা শেষ করেছি” এর শূন্যস্থানেই। একটি সহায়ক প্যাটার্ন হলো তিনটি আলাদা অর্থ আলাদা করা:

  • Received: বার্তাটি আপনার এজ-এ পৌঁছেছে (প্রয়োজনীয় না যে এটি সংরক্ষিত হয়েছে)
  • Accepted: আপনি এটি সংরক্ষণ করেছেন এবং পরে কাজ নিরাপদে রিট্রাই করতে পারেন
  • Processed: কাঙ্ক্ষিত কাজ সম্পন্ন হয়েছে এবং আপনি আউটকাম রেকর্ড করেছেন

যখন টিমগুলো এটি বাদ দেয়, তারা লোডের সময় বা আংশিক আউটেজের সময় ঘটতো এমন বাগ পায়। একটি সিস্টেম “paid” বলতে অর্থ করতে পারে টাকা ক্যাপচার করা; অন্যটি বলতে পারে পেমেন্ট চেষ্টা শুরু হয়েছে। সেই অমিল ডুপ্লিকেট, আটকে থাকা অর্ডার এবং এমন সাপোর্ট টিকিকেট তৈরি করে যেগুলো কেউ পুনরুত্পাদন করতে পারে না।

আইডেম্পোটেন্সি: রিট্রাইকে নিরাপদ করা

আইডেম্পোটেন্সি মানে: একই অনুরোধ যদি দুবার পাঠানো হয়, সিস্টেম সেটিকে এক অনুরোধের মত বিবেচনা করে এবং একই আউটকাম রিটার্ন করে।

রিট্রাই স্বাভাবিক। টাইমআউট ঘটে। ক্লায়েন্ট নিজেকে পুনরাবৃত্তি করে। যদি বাইরে পুনরাবৃত্তি করতে পারে, আপনার ভিতর এটিকে স্থিতিশীল স্টেট পরিবর্তনে পরিণত করতে হবে।

সরল উদাহরণ: একটি মোবাইল অ্যাপ "pay $20" পাঠায় এবং সংযোগ শেষ হয়ে যায়। অ্যাপ রিট্রাই করে। আইডেম্পোটেন্সি ছাড়া গ্রাহক দুবার চার্জ হতে পারে। আইডেম্পোটেন্সি থাকলে দ্বিতীয় অনুরোধ প্রথম চার্জের ফলই ফেরত দেয়।

আইডেম্পোটেন্সি বাস্তবায়নের প্রচলিত উপায়

বেশিরভাগ টিম এই প্যাটার্নগুলোর এক বা মিশ্রণ ব্যবহার করে:

  • Idempotency-Key: ক্লায়েন্ট প্রতিটি উদ্দেশ্যের জন্য একটি অনন্য কী পাঠায় (উদাহরণ: Idempotency-Key: ...)। সার্ভার কীগুলো এবং চূড়ান্ত রেসপন্স রেকর্ড করে।
  • De-duplication টেবিল: (client_id, key) বা (order_id, operation) দিয়ে কী-ভিত্তিক একটি সারি সংরক্ষণ করে দ্বিতীয় পাশ-এফেক্ট অস্বীকার করা।
  • Natural keys: এমন একটি ব্যবসায়িক শনাক্তকারী ব্যবহার করুন যা ইতিমধ্যেই অনন্য, তাই “create payment” কেবল একবারই থাকতে পারে।

যখন একটি ডুপ্লিকেট আসে, সাধারণত সেরা আচরণ হল "409 conflict" বা সাধারণ ত্রুটি না দেখিয়ে, একই রেসপন্স ফেরত দেওয়া — একই রিসোর্স ID এবং স্ট্যাটাস সহ। এটাই ক্লায়েন্ট এবং ব্যাকগ্রাউন্ড জবগুলোর জন্য রিট্রাইকে নিরাপদ করে।

রেকর্ড কোথায় রাখবেন (এবং কতদিন)

আইডেম্পোটেন্সি রেকর্ড অবশ্যই আপনার সীমানার ভিতরে টেকসই স্টোরেজে থাকতে হবে, মেমরিতে নয়। যদি API রিস্টার্ট করে এবং ভুলে যায়, সুরক্ষা গ্যারান্টি উধাও হয়ে যাবে।

বাস্তব রিট্রাই এবং বিলম্বিত ডেলিভারির কভার করার জন্য রেকর্ড যথেষ্ট দীর্ঘ রাখুন। উইন্ডো ব্যবসায়িক ঝুঁকির উপর নির্ভর করে: কম-ঝুঁকির ক্রিয়াগুলোর জন্য মিনিট থেকে ঘন্টা, পেমেন্ট/ইমেল/শিপমেন্টের মতো খরচী ক্ষেত্রের জন্য দিনগুলো এবং যদি পার্টনার দীর্ঘসময় রিট্রাই করতে পারে তবে আরও বেশি।

“বিস্তৃত ট্রানজ্যাকশন” ফাঁদগুলো কীভাবে এড়াবেন

আউটবক্স ও ইনবক্স বেসিক যোগ করুন
আউটবক্স এবং ইনবক্স টেবিল এবং হ্যান্ডলার জেনারেট করুন যা রিট্রাইয়ের সময় নিরাপদ থাকে।

বিতরণকৃত ট্রানজ্যাকশন আরামদায়ক শোনায়: সার্ভিস, কিউ এবং ডাটাবেস জুড়ে একটি বড় কমিট। বাস্তবে এগুলো প্রায়ই অনুপলব্ধ, ধীর বা অতিরিক্ত ভঙ্গুর হয়। একবার নেটওয়ার্ক হপ জড়িত হয়ে গেলে আপনি ধরতে পারবেন না সবকিছু একসাথে কমিট হবে।

একটি সাধারণ ফাঁদ হলো এমন একটি ওয়ার্কফ্লো তৈরি করা যা তখনই কাজ করে যদি প্রতিটি ধাপ তখনই সফল হয়: অর্ডার সেভ, কার্ড চার্জ, ইনভেন্টরি রিজার্ভ, কনফার্মেশন পাঠান। যদি ধাপ 3 টাইমআউট করে, তাহলে এটা ব্যর্থ না সফল—কী করবেন? যদি আপনি রিট্রাই করেন, কি আপনি ডাবল-চার্জ বা ডাবল-রিজার্ভ করবেন?

দুইটি ব্যবহারিক পদ্ধতি এড়াতে সহায়ক:

  • Outbox/inbox: আপনার ডাটাবেসে আপনার স্টেট পরিবর্তনের সাথে একই ট্রানজ্যাকশনে একটি আউটবক্স সারি লিখুন, তারপর একটি worker মেসেজ পাঠায়। রিসিভিং সাইডে একটি ইনবক্স রাখুন যা মেসেজ ID দ্বারা কীড থাকে যাতে একাধিকবার একই মেসেজ এলে নিরাপদভাবে হ্যান্ডল করা যায়।
  • Saga-স্টাইল ধাপ এবং ক্ষতিপূরণ: ওয়ার্কফ্লোকে ছোট ধাপে ভাঙ্গুন যেগুলো স্বাধীনভাবে সম্পূর্ণ হয়। যদি পরে কোনো ধাপ ব্যর্থ হয়, ইতিহাস রোলব্যাক করার পরিবর্তে একটি ক্ষতিপূরণ (উদাহরণ: ইনভেন্টরি রিলিজ বা অনপেইড অর্ডার বাতিল) চালান।

প্রতিটি ওয়ার্কফ্লোর জন্য একটি স্টাইল বেছে নিন এবং তার সাথে থাকুন। "কখনও কখনও আমরা আউটবক্স করি" আর "কখনও কখনও আমরা সিঙ্ক্রোনাস সাফল্য ধরে নিই" মিশালে এমন এজকেসগুলো তৈরি হয় যা টেস্ট করা কঠিন।

একটি সহজ নিয়ম: যদি আপনি সীমার ভেতরে অ্যাটোমিকালি কমিট করতে না পারেন, রিট্রাই, ডুপ্লিকেট এবং বিলম্বের জন্য ডিজাইন করুন।

রিকনসিলিয়েশন: বাস্তব সিস্টেম কিভাবে মিলবৈষম্য থেকে পুনরুদ্ধার করে

রিকনসিলিয়েশন একটি মৌলিক সত্য স্বীকার করে: যখন আপনার অ্যাপ নেটওয়ার্কের মাধ্যমে অন্য সিস্টেমের সঙ্গে কথা বলে, তখন মাঝে মাঝে আপনি কী হয়েছে সে নিয়ে অসম্মতি থাকবে। রিকুয়েস্ট টাইমআউট করে, কলব্যাক দেরিতে আসে, এবং মানুষ অ্যাকশন পুনরায় করে। রিকনসিলিয়েশন হল আপনি কিভাবে মিলবৈষম্য শনাক্ত করে সময়ের সাথে তা ঠিক করবেন।

বাইরের সিস্টেমগুলোকে স্বাধীন সত্যের উৎস হিসেবে বিবেচনা করুন। আপনার অ্যাপের নিজের অভ্যন্তরীণ রেকর্ড আছে, কিন্তু এটি পার্টনার, প্রদানকারী এবং ব্যবহারকারীর সাথে মিলিয়ে তুলতে একটি উপায় চাই।

প্রচলিত রিকনসিলিয়েশন মেকানিজম

বেশিরভাগ টিম ব্যবহার করে কিছু সাধারণ কারণের টুল (সহজ হলেও ভালো): একটি worker যা পেন্ডিং অ্যাকশনগুলো পুনরায় ট্রাই করে এবং বহির্ভূত স্ট্যাটাস রি‑চেক করে, অনিয়মিততার জন্য শিডিউলড স্ক্যান, এবং সাপোর্টের জন্য ছোট একটি অ্যাডমিন রিপেয়ার অ্যাকশন (রিট্রাই, বাতিল, বা রিভিউ হিসেবে চিহ্নিত)।

কি তুলনা করবেন এবং কি রেকর্ড করবেন

রিকনসিলিয়েশন কাজ করবে যদি আপনি জানেন কি তুলনা করবেন: অভ্যন্তরীণ লেজার বনাম প্রদানকারী লেজার (পেমেন্ট), অর্ডার স্টেট বনাম শিপমেন্ট স্টেট (ফুলফিলমেন্ট), সাবস্ক্রিপশন স্টেট বনাম বিলিং স্টেট।

স্টেটগুলোকে রেপেয়ারযোগ্য রাখুন। সরাসরি “created” থেকে “completed” তে যাওয়ার পরিবর্তে হোল্ডিং স্টেট যেমন pending, on hold, বা needs review ব্যবহার করুন। এটি বলতে নিরাপদ করে "আমরা নিশ্চিত না" এবং রিকনসিলিয়েশনের একটি স্পষ্ট ল্যান্ডিং জায়গা দেয়।

গুরুত্বপূর্ণ পরিবর্তনগুলোর উপর একটি ছোট অডিট ট্রেইল ধরুন:

  • কখন আপনি একটি অনুরোধ পাঠিয়েছিলেন এবং কখন শেষবার ফিরতি শুনেছিলেন
  • correlation ID যা আপনার রেকর্ডকে একটি বাহ্যিক ইভেন্ট/রেফারেন্সের সাথে যুক্ত করে
  • সর্বশেষ জানাযাওয়া বহির্ভূত স্ট্যাটাস (এবং এটি কোথা থেকে এসেছে)
  • ম্যানুয়াল ওভাররাইডের কারণ ফিল্ড (কে, কি, কেন)

উদাহরণ: যদি আপনার অ্যাপ শিপমেন্ট লেবেল অনুরোধ করে এবং নেটওয়ার্ক কেটে যায়, আপনি অভ্যন্তরে "কোন লেবেল নেই" পেতে পারেন যখন ক্যারিয়ার বাস্তবে একটি লেবেল তৈরি করেছে। একটি recon worker correlation ID দিয়ে খুঁজে বের করতে পারে যে লেবেল আছে এবং অর্ডারকে এগিয়ে নিয়ে যেতে পারে (অথবা যদি ডিটেইল মেল না খায়, রিভিউর জন্য চিহ্নিত করবে)।

ধাপে ধাপে: নেটওয়ার্ক ব্যর্থতা টিকে টিকে থাকার মতো একটি ওয়ার্কফ্লো ডিজাইন করা

ডুপ্লিকেট ছাড়া webhook হ্যান্ডল করুন
ডিডুপ, স্টোর করা ইভেন্ট আইডি, এবং একটি অডিট ট্রেইল সহ webhook হ্যান্ডলার তৈরি করুন।

একবার আপনি ধরে নিলেন নেটওয়ার্ক ব্যর্থ হবে, লক্ষ্য বদলে যায়। প্রতিটি ধাপ একবারেই সফল করার চেষ্টা নয়; বরং প্রতিটি ধাপকে পুনরাবৃত্তি করা নিরাপদ ও মেরামত করা সহজ করা।

একটি ব্যবহারিক ওয়ার্কফ্লো

  1. একটি এক-লাইন সীমানা বিবৃতি লিখুন। আপনি স্পষ্টভাবে কি আপনার সিস্টেম owns (অর্থাৎ truth), কি এটি mirrors করে, এবং কি শুধুমাত্র অন্যদের কাছে অনুরোধ করে তা উল্লেখ করুন।

  2. সুখী পথ আগে নয়—ব্যর্থতা মোডগুলো তালিকাভুক্ত করুন। কমপক্ষে: টাইমআউট (আপনি জানেন না এটা কাজ করেছে কিনা), ডুপ্লিকেট রিকুয়েস্ট, আংশিক সাফল্য (এক ধাপ হয়েছে, পরেরটা হয়নি), এবং ভুল ক্রমে ইভেন্ট।

  3. প্রতিটি ইনপুটের জন্য একটি আইডেম্পোটেন্সি কৌশল চয়ন করুন। synchronous API এর জন্য প্রায়শই idempotency key + সংরক্ষিত ফলাফল; মেসেজ/ইভেন্টের জন্য একটি অনন্য মেসেজ ID এবং "আমি কি এটি প্রসেস করেছি?" রেকর্ড।

  4. উদ্দেশ্য স্থায়ীভাবে সংরক্ষণ করুন, তারপর কাজ করুন। প্রথমে কিছু টেকসই সংরক্ষণ করুন যেমন PaymentAttempt: pending বা ShipmentRequest: queued, তারপর বহির্ভূত কল করুন, পরে আউটকাম সংরক্ষণ করুন। একটি স্থাবর রেফারেন্স ID রিটার্ন করুন যাতে রিট্রাই একই উদ্দেশ্যের দিকে পয়েন্ট করে নতুন একটি তৈরি না করে।

  5. রিকনসিলিয়েশন এবং মেরামতের পথ তৈরি করুন, এবং সেগুলো দৃশ্যমান করুন। রিকনসিলিয়েশন হতে পারে এমন একটি জব যা "অতিরিক্ত সময় পেন্ডিং" রেকর্ডগুলি স্ক্যান করে এবং স্ট্যাটাস পুনরায় চেক করে। মেরামত পথটি হতে পারে একটি নিরাপদ অ্যাডমিন অ্যাকশন যেমন "retry", "cancel" বা "mark resolved" একটি অডিট নোট সহ। মৌলিক অবজার্ভেবিলিটি যোগ করুন: correlation ID, পরিষ্কার স্ট্যাটাস ফিল্ড, এবং কিছু কনটিং (pending, retries, failures)।

উদাহরণ: যদি চেকআউট টাইমআউট করে ঠিক পরে আপনি পেমেন্ট প্রদানকারীকে কল করেছেন, আন্দাজ করবেন না। চেষ্টা সংরক্ষণ করুন, attempt ID রিটার্ন করুন, এবং ব্যবহারকারীকে একই idempotency কী দিয়ে পুনরায় চেষ্টা করার সুযোগ দিন। পরে রিকনসিলিয়েশন নিশ্চিত করবে প্রদানকারী চার্জ করেছে কি না এবং ডাবল-চার্জ না করে attempt আপডেট করবে।

উদাহরণ দৃশ্য: রিট্রাই এবং বিলম্বিত কলব্যাকসহ একটি অর্ডার ফ্লো

একটি গ্রাহক "Place order" ট্যাপ করে। আপনার সার্ভিস একটি পেমেন্ট অনুরোধ প্রদানকারীকে পাঠায়, কিন্তু নেটওয়ার্ক অনিচ্ছিন্ন। প্রদানকারীর নিজেদের একটি সত্য আছে, এবং আপনার ডাটাবেসের নিজের। যদি আপনি ডিজাইন না করেন তবে এরা বিচ্ছিন্ন হবে।

বাইরের দিক থেকে কী ঘটে (আপনি নিয়ন্ত্রণ করেন না)

আপনার দৃষ্টিকোণ থেকে বাইরে একটি স্ট্রিম বার্তা যা দেরিতে, পুনরাবৃত্ত বা অনুপস্থিত হতে পারে:

  • "Submit order" আপনার API-এ পৌঁছে।
  • আপনার পেমেন্ট অনুরোধ প্রদানকারীকে যায়।
  • প্রদানকারী একটি webhook পাঠায় যে "authorized"।
  • প্রদানকারী webhook পুনরায় পাঠায় এবং একই কলব্যাক আবার পাঠায়।
  • আপনার ক্লায়েন্ট টাইমআউট হয় এবং "Place order" পুনরায় রিকুয়েস্ট করে।

এই ধাপগুলো কোনোটাই "একবারই"-এর গ্যারান্টি দেয় না। সেগুলো মাত্র "সম্ভব" বলে গ্যারান্টি দেয়।

ভেতরে আপনি কী রাখেন (আপনি নিয়ন্ত্রণ করেন এমন রেকর্ড)

আপনার সীমানার ভিতরে, টেকসই তথ্য ও বাহ্যিক ইভেন্টগুলোকে ঐ তথ্যের সঙ্গে যুক্ত করার জন্য নূন্যতম দরকারি ডেটা সংরক্ষণ করুন।

গ্রাহক প্রথমবার অর্ডার করলে একটি order রেকর্ড তৈরি করুন একটি স্পষ্ট স্টেটে যেমন pending_payment। একই সাথে একটি payment_attempt রেকর্ড তৈরি করুন যা একটি ইউনিক প্রদানকারী রেফারেন্স এবং idempotency_key ধারণ করবে যা গ্রাহকের কর্মকান্ডের সাথে связанных।

যদি ক্লায়েন্ট টাইমআউট করে এবং পুনরায় চেষ্টা করে, আপনার API-কে দ্বিতীয় অর্ডার তৈরি করা উচিত নয়। এটি idempotency_key দেখে একই order_id এবং বর্তমান স্টেট রিটার্ন করা উচিত। ওই সিদ্ধান্তই নেটওয়ার্ক ব্যর্থ হলে ডুপ্লিকেট প্রতিরোধ করে।

এখন webhook দুটি বার আসে। প্রথম কলব্যাক payment_attempt কে authorized করে আপডেট করে এবং অর্ডারকে paid এ নিয়ে যায়। দ্বিতীয় কলব্যাক একই হ্যান্ডলারকে আঘাত করে, কিন্তু আপনি দেখতে পাবেন যে আপনি ইতিমধ্যেই সেই প্রদানকারী ইভেন্টটি প্রসেস করেছেন (প্রদানকারী ইভেন্ট ID সংরক্ষণ করে, বা বর্তমান স্টেট চেক করে) এবং কিছুই করবেন না। আপনি এখনও 200 OK রেসপন্স পাঠাতে পারেন, কারণ রেজাল্টটি ইতিমধ্যেই সত্য।

শেষমেষ, রিকনসিলিয়েশন মিশ্র কেসগুলো সামলে নেয়। যদি একটি অর্ডার একটি বিলম্বিত সময় পরে এখনও pending_payment থাকে, একটি ব্যাকগ্রাউন্ড জব সংরক্ষিত রেফারেন্স ব্যবহার করে প্রদানকারীকে জিজ্ঞাসা করবে। যদি প্রদানকারী বলে "authorized" কিন্তু আপনি webhook মিস করেছেন, আপনি আপনার রেকর্ড আপডেট করবেন। যদি প্রদানকারী বলে "failed" কিন্তু আপনি ইতিমধ্যেই paid চিহ্নিত করেছেন, আপনি সেটি রিভিউর জন্য ফ্ল্যাগ করবেন বা ক্ষতিপূরণমূলক ক্রিয়া (যেমন রিফান্ড) ট্রিগার করবেন।

ডুপ্লিকেট এবং আটকে থাকা স্টেট তৈরির কমন ভুলগুলো

ব্যর্থতা কেসগুলো নিরাপদে টেস্ট করুন
স্ন্যাপশট এবং রোলব্যাক ব্যবহার করে গুরুত্বপূর্ণ ওয়ার্কফ্লো নিরাপদে পুনরাবৃত্তি করুন।

অধিকাংশ ডুপ্লিকেট রেকর্ড এবং "আটকে থাকা" ওয়ার্কফ্লো আসে বাইরের ঘটনাকে (একটি রিকুয়েস্ট এসেছে, একটি বার্তা প্রাপ্ত হয়েছে) এবং আপনি যা নিরাপদে আপনার ভিতরে কমিট করেছেন সেটাকে মিশিয়ে ফেলার কারণে।

একটি ক্লাসিক ব্যর্থতা: ক্লায়েন্ট "place order" পাঠায়, আপনার সার্ভার কাজ শুরু করে, নেটওয়ার্ক ড্রপ করে, এবং ক্লায়েন্ট পুনরায় চেষ্টা করে। যদি আপনি প্রতিটি রিট্রাইকে নতুন সত্য মনে করেন, আপনি ডাবল চার্জ, ডুপ্লিকেট অর্ডার, বা একাধিক ইমেল তৈরি করবেন।

সাধারণ কারণগুলো:

  • Incoming request-কে খুব তাড়াতাড়ি বিশ্বাস করা: ডাটাবেস কমিটের আগে ইমেল পাঠানো বা "order created" লগ করা নিরাপদ নয়।
  • রিট্রাইগুলো নতুন সারি তৈরি করে: প্রতিটি প্রচেষ্টায় নতুন order ID জেনারেট করা বরং রিট্রাইকেই একই আউটকামে মানচিত্র করা উচিত।
  • "একবারই ঠিক" ডেলিভারির ধারণা: কিউ ও কলব্যাকগুলো সেই প্রতিশ্রুতি দেয় না। ডুপ্লিকেট, বিলম্ব এবং রি‑অর্ডারিং ঘটে।
  • স্থায়ী আইডেন্টিফায়ার নেই: যদি আপনি উত্তর না দিতে পারেন "আমি কি এই নির্দিষ্ট উদ্দেশ্যটি আগে দেখেছি?", আপনি ডুপ্লিকেট আটকাতে পারবেন না।
  • শুধু success/failure, মাঝখানের স্টেট নেই: pending/awaiting স্টেট না থাকলে টাইমআউট রহস্যে পরিণত হয় এবং ব্যবহারকারী আবার ক্লিক করে।

একটি সমস্যা সবকিছুকে খারাপ করে: কোনও অডিট ট্রেইল নেই। যদি আপনি ফিল্ডগুলো ওভাররাইট করে কেবল সাম্প্রতিক স্টেট রাখেন, আপনি পরে রিকনসিলিয়েশনের জন্য প্রয়োজনীয় প্রমাণ হারিয়ে ফেলেন।

একটি ভাল সম্প্রীতি পরীক্ষা: "আমি যদি এই হ্যান্ডলারটি দুবার চালাই, কি একই ফল পাব?" যদি উত্তর না হয়, ডুপ্লিকেট রেয়ার এজকেস নয়—এগুলো গ্যারান্টিত।

দ্রুত চেকলিস্ট ও ব্যবহারিক পরবর্তী ধাপ

যদি আপনি একটাই মনে রাখেন: আপনার অ্যাপকে সঠিক থাকতে হবে এমনকি যখন বার্তাগুলো দেরিতে আসে, দুবার আসে বা একেবারেই না আসে।

এই চেকলিস্টটি দুর্বল পয়েন্টগুলো চিহ্নিত করতে ব্যবহার করুন আগে তারা ডুপ্লিকেট রেকর্ড, অনুপস্থিত আপডেট, বা আটকে থাকা ওয়ার্কফ্লো তৈরি করে:

  • Source of truth স্পষ্ট: প্রতিটি ওয়ার্কফ্লোর জন্য আপনি একটি জায়গা বলতে পারেন যা "সত্য" (সাধারণত আপনার ডাটাবেস)।
  • প্রতিটি লিখন নিরাপদে রিট্রাই করা যায়: প্রতিটি কমান্ড/API কলের জন্য একটি idempotency কী (বা একটি ন্যাচারাল ইউনিক কী) আছে।
  • স্টেবল ID এবং correlation ID আছে end-to-end: আপনি এক ব্যবসায়িক কাজকে লগ, টেবিল এবং কলব্যাক জুড়ে ট্রেস করতে পারেন।
  • রিকনসিলিয়েশন স্বয়ংক্রিয়ভাবে চলে: আপনি নিয়মিতভাবে "আমরা যা বিশ্বাস করি" বনাম "কি ঘটেছে" তুলনা করে মেরামত বা স্পষ্ট অ্যালার্ট তুলেন।
  • রোলব্যাক স্টেটকে দূষিত করে না: স্টেট পরিবর্তনগুলো অডিটেবল এবং সংস্করণগুলোর মধ্যে সামঞ্জস্যপূর্ণ।

আপনি যদি এগুলোর কোনটির দ্রুত উত্তর না দিতে পারেন, এটি সাধারণত মানে একটি সীমা অস্পষ্ট বা একটি স্টেট ট্রানজিশন অনুপস্থিত।

ব্যবহারিক পরবর্তী ধাপ:

  1. প্রথমে সীমানা এবং স্টেটগুলো আঁকুন। প্রতিটি ওয়ার্কফ্লোর জন্য ছোট একটি স্টেট সেট সংজ্ঞায়িত করুন (উদাহরণ: Created, PaymentPending, Paid, FulfillmentPending, Completed, Failed)।

  2. সবচেয়ে ঝুঁকিপূর্ণ লিখনে আইডেম্পোটেন্সি যোগ করুন। শুরু করুন: create order, capture payment, issue refund। PostgreSQL-এ ইউনিক কনস্ট্রেইন্টসহ idempotency কী সংরক্ষণ করুন যাতে ডুপ্লিকেট নিরাপদে প্রত্যাখ্যাত হয়।

  3. রিকনসিলিয়েশনকে একটি সাধারণ ফিচার হিসেবে বিবেচনা করুন। একটি জব শিডিউল করুন যা "অতিরিক্ত সময় পেন্ডিং" রেকর্ডগুলো খোঁজে, বহির্ভূত সিস্টেম চেক করে, এবং লোকাল স্টেট মেরামত করে।

  4. নিরাপদে পুনরাবৃত্তি করুন। ট্রানজিশন এবং রিট্রাই নিয়ম ঠিক করুন, তারপর একই অনুরোধ পুনরায় পাঠিয়ে এবং একই ইভেন্ট পুনরায় প্রসেস করে পরীক্ষা করুন।

আপনি যদি দ্রুতভাবে Koder.ai (koder.ai) মত একটি চ্যাট-চালিত প্ল্যাটফর্মে তৈরি করছেন, তখনও এগুলো প্রথম থেকেই আপনার জেনারেটেড সার্ভিসে সংযুক্ত করা মূল্যবান: গতি অটোমেশনের থেকে আসে, কিন্তু নির্ভরযোগ্যতা আসে স্পষ্ট সীমানা, আইডেম্পোটেন্ট হ্যান্ডলার এবং রিকনসিলিয়েশন থেকে।

সাধারণ প্রশ্ন

সরল ভাষায় “বাইরের বনাম ভেতরের ডেটা” মানে কী?

"বাইরে" হল এমন সবকিছু যা আপনি নিয়ন্ত্রণ করেন না: ব্রাউজার, মোবাইল নেটওয়ার্ক, কিউ, তৃতীয়-পক্ষের webhook, রিট্রাই এবং টাইমআউট। বার্তা বিলম্বিত, সদৃশ, হারিয়ে যেতে পারে বা সঠিক ক্রমে না আসতে পারে এমনটি ধরে নিন.

"ভিতরে" হল আপনি যা নিয়ন্ত্রণ করেন: আপনার সংরক্ষিত স্টেট, আপনার নিয়মগুলো এবং পরে প্রমাণ করার মতো তথ্য (সাধারণত আপনার ডেটাবেসে)।

আমি কেন ইনকামিং রিকুয়েস্ট বা webhook-কে একবারই ঘটবে বলে বিশ্বাস করতে পারি না?

কারণ নেটওয়ার্ক আপনাকে মিথ্যা বলে।

ক্লায়েন্ট টাইমআউট হওয়া মানেই আপনার সার্ভার রিকুয়েস্ট প্রসেস করেনি এমন নয়। একটি webhook দুবার এসে গেলেও সেটি প্রদানকারী কাজটি দুবার করেছে এমন অর্থ নাও বহন করে। যদি আপনি প্রতিটি বার্তাকে "নতুন সত্য" ধরা শুরু করেন, তাহলে ডুপ্লিকেট অর্ডার, ডাবল চার্জ এবং আটকে থাকা ওয়ার্কফ্লো তৈরি হবে।

সাধারণ অ্যাপে আমি কোথায় “সীমানা” টানব?

একটি পরিষ্কার সীমানা হলো যেখানে একটি অনিশ্চিত বার্তা একটি টেকসই সত্যে পরিণত হয়।

সাধারণ সীমান্তগুলো:

  • ডেটাবেসে কমিট করে এমন একটি API endpoint
  • একটি ইভেন্টকে স্টেট পরিবর্তনে রূপান্তরকারী queue consumer
  • একটি webhook হ্যান্ডলার যা প্রদানকারী কি ঘটেছে সেটা রেকর্ড করে

একবার ডেটা সীমান্ত পেরিয়ে গেলে আপনি ভিতরে ইনভারিয়েন্টগুলো প্রয়োগ করেন (যেমন "অর্ডার একবারই পেইড হতে পারে").

ব্যবহারকারী যখন “Pay” রিট্রাই করে তখন ডাবল চার্জ কিভাবে আটকাব?

আইডেম্পোটেন্সি ব্যবহার করুন। মৌলিক ধারণা: একই উদ্দেশ্য যদি একাধিকবার পাঠানো হয়, সিস্টেম সেটিকে একবারই ঘটবে বলে বিবেচনা করবে এবং একই আউটকাম রিটার্ন করবে.

প্র্যাকটিক্যাল প্যাটার্ন:

  • ক্লায়েন্ট প্রত্যেক অ্যাকশনের জন্য একটি idempotency কী পাঠায়
  • সার্ভার কী এবং চূড়ান্ত ফলাফল টেকসইভাবে সংরক্ষণ করে
  • ডুপ্লিকেট এলে একই রিসোর্স ID/স্ট্যাটাস ফিরিয়ে দিন যা প্রথমবার দিয়েছিল
আমি কোথায় আইডেম্পোটেন্সি রেকর্ড সংরক্ষণ করব এবং কতক্ষণ রাখব?

মনে রাখবেন এটি কেবল মেমরিতে রাখবেন না। এটি আপনার সীমানার ভিতরে (উদাহরণস্বরূপ PostgreSQL) সংরক্ষণ করুন যাতে রিস্টার্টেও সুরক্ষা বজায় থাকে.

সংরক্ষণকাল কেমন হবে তা ঝুঁকির উপর নির্ভর করবে:

  • কম ঝুঁকির ক্রিয়াগুলি: কয়েক মিনিট থেকে কয়েক ঘন্টা
  • উচ্চ-খরচের ক্রিয়াগুলি (পেমেন্ট, রিফান্ড, শিপিং, ইমেল): কয়েকদিন বা তার বেশি

এটি যথেষ্ট দীর্ঘ রাখুন যাতে বাস্তবসম্মত রিট্রাই এবং বিলম্বিত ডেলিভারি ঢাকা পড়ে।

কোন স্টেটগুলো যোগ করা উচিত যাতে “নিশ্চিত না হওয়া” বাগগুলো এড়ানো যায়?

অনিশ্চয়তাকে স্বীকার করে এমন স্টেট ব্যবহার করুন।

সরল এবং ব্যবহারিক একটি সেট:

  • pending_* (আমরা উদ্দেশ্যটি গ্রহণ করেছি কিন্তু ফলাফল জানি না)
  • succeeded / failed (চূড়ান্ত আউটকাম রেকর্ড করা হয়েছে)
  • needs_review (মিলমেল দেখা গেছে যা একজন মানুষের হস্তক্ষেপ বা বিশেষ জব চায়)

এটি টাইমআউটের সময় জিগার করা থেকে বিরত রাখে এবং রিকনসিলিয়েশন সহজ করে তোলে।

কেন ডিসট্রিবিউটেড ট্রানজ্যাকশন অ্যাপ ওয়ার্কফ্লো-এর জন্য সাধারণত একটা ফাঁদ?

কারণ আপনি নেটওয়ার্ক জুড়ে একাধিক সিস্টেমে অ্যাটোমিক কমিট ধরতে পারবেন না।

যদি আপনি "অর্ডার সেভ → কার্ড চার্জ → ইনভেন্টরি রিজার্ভ" সিঙ্ক্রোনাসভাবে করেন এবং ধাপ 2 টাইমআউট করে, তাহলে আপনি জানবেন না সেটা ব্যর্থ হয়েছে নাকি সফল। রিট্রাই করলে ডুপ্লিকেট হবে; না করলে কাজ অসম্পূর্ণ থাকবে.

পার্শিয়াল সাফল্যের জন্য ডিজাইন করুন: প্রথমে উদ্দেশ্যটি টেকসইভাবে সংরক্ষণ করুন, তারপর এক্সটার্নাল অ্যাকশন করুন, পরে আউটকাম রেকর্ড করুন।

outbox/inbox প্যাটার্ন কি, এবং কখন ব্যবহার করা উচিত?

outbox/inbox প্যাটার্ন নেটওয়ার্ককে পারফেক্ট মনে না করে ক্রস-সিস্টেম মেসেজিং নির্ভরযোগ্য করে তোলে.

  • Outbox: আপনার স্টেট পরিবর্তনের একই ট্রানজ্যাকশনে একটি সারি লিখুন যা পাঠানোর উদ্দেশ্য বোঝায়।
  • একটি worker outbox পড়ে এবং মেসেজ পাঠায়।
  • Inbox (রিসিভার সাইড): প্রসেস করা মেসেজ ID সংরক্ষণ করুন যাতে রি‑ডেলিভারি ডুপ্লিকেট সাইড-এফেক্ট না করে।
রিকনসিলিয়েশন কি, এবং এটা সহজভাবে কীভাবে বাস্তবায়ন করবেন?

রিকনসিলিয়েশন হলো যখন আপনার রেকর্ড এবং একটি বাহ্যিক সিস্টেমের মধ্যে অমিল দেখা দেয় তখন কিভাবে আপনি পুনরুদ্ধার করবেন।

ভাল ডিফল্ট:

  • একটি শিডিউলড জব যা "দীর্ঘ সময় পেন্ডিং" আইটেমগুলো পুনরায় চেক করে
  • একটি তুলনা ধাপ (আপনার স্টেট বনাম প্রদানকারীর স্টেট)
  • একটি মেরামত ক্রিয়া: রিট্রাই, বাতিল, রিফান্ড, অথবা needs_review চিহ্নিত করা

পেমেন্ট, ফুলফিলমেন্ট, সাবস্ক্রিপশন বা যেকোনো webhook-যুক্ত কাজের জন্য এটি অপরিহার্য।

আমি যদি Koder.ai-এর মত প্ল্যাটফর্মে দ্রুত তৈরি করছি তাহলে কি এটা গুরুত্বপূর্ণ?

হ্যাঁ। দ্রুত তৈরি করা নেটওয়ার্ক ব্যর্থতা বিলোপ করে না—এরা আপনাকে দ্রুতই সমস্যার মুখোমুখি করে।

যদি আপনি Koder.ai (koder.ai)‑এর মতো প্ল্যাটফর্মে সার্ভিস দ্রুত জেনারেট করে থাকেন, এই ডিফল্টগুলো আগে থেকেই অন্তর্ভুক্ত করা মূল্যবান:

  • স্পষ্ট সীমানা (কোথায় একটি উদ্দেশ্য টেকসই হয়)
  • "create/capture/refund" ধাঁচের একশনগুলোর জন্য আইডেম্পোটেন্ট হ্যান্ডলার
  • বাহ্যিক রেফারেন্সগুলোর সাথে সংযুক্ত correlation ID গুলো সংরক্ষিত
  • পেন্ডিং রেকর্ডগুলোর জন্য একটি রিকনসিলিয়েশন জব

এভাবে, রিট্রাই এবং ডুপ্লিকেট কলব্যাক সাধারণ ঝামেলা না হয়ে নির্বিঘ্ন কাজ করবে।

Related posts