২০ ডিসে, ২০২৫·6 মিনিট পড়তে

নির্ভরযোগ্য API ইন্টিগ্রেশনের জন্য PostgreSQL-এ আউটবক্স প্যাটার্ন

PostgreSQL-এ আউটবক্স প্যাটার্ন শিখুন: ইভেন্টগুলো ডাটাবেসে রাখুন, তারপর থার্ড-পার্টি API-তে রিট্রাই, অর্ডারিং ও ডুপ্লিকেট প্রতিরোধ সহ ডেলিভারি করুন।

নির্ভরযোগ্য API ইন্টিগ্রেশনের জন্য PostgreSQL-এ আউটবক্স প্যাটার্ন

কেন আপনার অ্যাপ কাজ করলেও ইন্টিগ্রেশন ফেল হয়\n\nপ্রায়শই দেখা যায় যে আপনার অ্যাপে কোনো কাজ “সফল” দেখায় কিন্তু তার পেছনের ইন্টিগ্রেশন নিঃশব্দে ব্যর্থ হয়। আপনার ডাটাবেস লেখা দ্রুত ও নির্ভরযোগ্য—আর তৃতীয় পক্ষের API কল তা নয়। এতে দুটি পৃথক জগত তৈরি হয়: আপনার সিস্টেম বলে পরিবর্তন হয়েছে, কিন্তু বাহ্যিক সিস্টেম কখনো তা শুনেনি।\n\nএকটি সাধারণ উদাহরণ: কাস্টমার একটি অর্ডার দেয়, আপনার অ্যাপ সেটি PostgreSQL-এ সেভ করে, এবং তারপর শিপিং প্রোভাইডারকে নোটিফাই করার চেষ্টা করে। যদি প্রোভাইডার 20 সেকেন্ডে টাইমআউট করে এবং আপনার অনুরোধ ছেড়ে দেয়, তবুও অর্ডার বাস্তব, কিন্তু শিপমেন্ট কখনো তৈরি হয়নি।\n\nইউজাররা এটি বিভ্রান্তিকর, অসংগত আচরণ হিসেবে অনুভব করে। হারিয়ে যাওয়া ইভেন্টগুলো মনে হয় “কিছুই হয়নি।” ডুপ্লিকেট ইভেন্টগুলো হয় “কেন আমাকে দুবার চার্জ করা হল?” সাপোর্ট টিমও কষ্ট পায় কারণ বোঝা কঠিন সমস্যা কোথায়—আপনার অ্যাপ, নেটওয়ার্ক, না পার্টনার কি।\n\nরিট্রাই সাহায্য করে, কিন্তু কেবল রিট্রাই যথার্থতা নিশ্চিত করে না। যদি আপনি টাইমআউটের পরে রিট্রাই করেন, তাহলে আপনি একই ইভেন্ট দুবার পাঠাতে পারেন কারণ আগে পার্টনার প্রথম অনুরোধ পেয়েছে কি না তা জানেন না। যদি আপনি ভুল ক্রমে রিট্রাই করেন, তাহলে হতে পারে “Order shipped” আগে পৌঁছায় “Order paid” এর আগে।\n\nএসব সমস্যা সাধারণ কনকারেন্সি থেকে আসে: অনেক ওয়ার্কার প্যারালাল প্রসেস করে, একাধিক অ্যাপ সার্ভার একই সময়ে লিখে, এবং “বেস্ট ইফোর্ট” কিউগুলো লোডে সময় পরিবর্তন করে। ব্যর্থতার মোডগুলো ভবিষ্যদ্বানী: API ডাউন বা ধীর, নেটওয়ার্ক অনুরোধ হারায়, প্রসেস ক্র্যাশ করে ভুল মুহূর্তে, এবং রিট্রাই ডুপ্লিকেট তৈরি করে যখন কিছুই আইডেমপোটেন্সি বলবিশেষ নিশ্চিত করে না।\n\nএই স্বাভাবিক ব্যর্থতার কারণেই আউটবক্স প্যাটার্ন আছে।\n\n## সরল কথায় আউটবক্স প্যাটার্ন কী\n\nআউটবক্স প্যাটার্ন সহজ: যখন আপনার অ্যাপ কোনো গুরুত্বপূর্ণ পরিবর্তন করে (যেমন অর্ডার তৈরি), তখন একই ট্রানজ্যাকশনে ডাটাবেস টেবিলের মধ্যে একটি ছোট “পাঠানোর জন্য ইভেন্ট” রেকর্ডও লিখে। যদি ডাটাবেস কমিট সফল হয়, আপনি জানেন ব্যবসায়িক ডেটা ও ইভেন্ট রেকর্ড একসাথে আছে।\n\nএরপর একটি আলাদা ওয়ার্কার আউটবক্স টেবিল পড়ে এবং সেই ইভেন্টগুলো তৃতীয় পক্ষের API-তে ডেলিভারি করে। যদি কোনো API ধীর, ডাউন বা টাইমআউট করে, আপনার মূল ইউজার রিকোয়েস্ট তখনও সফল থাকবে কারণ এটি বাইরের কলের জন্য অপেক্ষা করে না।\n\nএটি সেই অদ্ভুত অবস্থাগুলো এড়ায় যা আপনি রিকোয়েস্ট হ্যান্ডলারের ভিতরে API কল করলে পাই:\n\n- অর্ডার সেভ হয়েছে, কিন্তু API কল ব্যর্থ।\n- API কল সফল হয়েছে, কিন্তু আপনার অ্যাপ ক্র্যাশ করে আগে অর্ডার সেভ হওয়ার আগেই।\n- ইউজার রিট্রাই করে, এবং আপনি একই জিনিস দুবার পাঠান।\n\nআউটবক্স প্যাটার্ন মূলত হারিয়ে যাওয়া ইভেন্ট, আংশিক ব্যর্থতা (ডাটাবেস ঠিক আছে, বহিরাগত API নেই), অযথা ডাবল সেন্ড এবং নিরাপদ রিট্রাই (পরে আবার চেষ্টা করা যায় অনুমান ছাড়াই) মোকাবিলা করে।\n\nএটি সবকিছু ঠিক করে না। যদি আপনার পেআলোড ভুল হয়, ব্যবসায়িক নিয়ম ভুল হয়, বা তৃতীয় পক্ষের API ডেটা প্রত্যাখ্যান করে, তখনও আপনাকে ভ্যালিডেশন, ভাল এরর হ্যান্ডলিং, এবং ব্যর্থ ইভেন্ট পরিদর্শন ও সংশোধনের উপায় রাখতে হবে।\n\n## PostgreSQL-এ আউটবক্স টেবিল ডিজাইন করা\n\nএকটি ভাল আউটবক্স টেবিল ইচ্ছাকৃতভাবে সাধারণ হওয়া উচিত। এটি লিখতে সহজ, পড়তে সহজ এবং ভুলভাবে ব্যবহার করা কঠিন হওয়া উচিত।\n\nএখানে একটি প্রয়োগযোগ্য বেসলাইন স্কিমা যা আপনি অভিযোজিত করতে পারেন:\n\n```sql

create table outbox_events ( id bigserial primary key, aggregate_id text not null, event_type text not null, payload jsonb not null, status text not null default 'pending', created_at timestamptz not null default now(), available_at timestamptz not null default now(), attempts int not null default 0, locked_at timestamptz, locked_by text, meta jsonb not null default '{}'::jsonb ); ```\n\n### একটি ID নির্বাচন করা\n\nbigserial (বা bigint) ব্যবহার করলে অর্ডারিং সহজ থাকে এবং ইনডেক্স দ্রুত থাকে। UUID সিস্টেম জুড়ে ইউনিকনেস ভালো দেয়, কিন্তু সেগুলো সৃষ্টি-ক্রমে সাজায় না, যা polling কে অপ্রেডিক্টেবল করতে পারে এবং ইনডেক্স ভারী করে।\n\nএকটি সাধারণ সমঝোতা: অর্ডারিংয়ের জন্য id কে bigint রাখুন, এবং যদি সার্ভিস জুড়ে একটি স্থায়ী শনাক্তকারী দরকার হয় তাহলে আলাদা event_uuid যোগ করুন।\n\n### গুরুত্বপূর্ণ ইনডেক্সগুলো\n\nআপনার ওয়ার্কার সারাদিন একই প্যাটার্নে কুয়েরি করবে। বেশিরভাগ সিস্টেমে দরকার:\n\n- (status, available_at, id) ধরনের একটি ইনডেক্স যাতে পরবর্তী পেন্ডিং ইভেন্টগুলো অর্ডারভিত্তিক ফেচ করা যায়।\n- যদি স্টেল লকসমূহ মেয়াদ উত্তীর্ণ করতে চান তাহলে (locked_at) এর উপর ইনডেক্স।\n- কখনো কখনো যদি আপনি অ্যাগ্রিগেট অনুসারে ডেলিভারি করেন তাহলে (aggregate_id, id) এর মতো ইনডেক্স।\n\n### পে-লোডগুলো স্থিতিশীল রাখুন\n\nপে-লোডগুলো ছোট ও পূর্বনিয়ন্ত্রিত রাখুন। রিসিভার যা প্রয়োজন তা স্টোর করুন, আপনার পুরো রো নয়। একটি স্পষ্ট ভার্সন (meta তে যেমন) রাখুন যাতে আপনি ক্ষেত্রগুলো নিরাপদে ইভলভ করতে পারেন।\n\nরাউটিং এবং ডিবাগিং কনটেক্সট যেমন টেন্যান্ট ID, করিলেশন ID, ট্রেস ID, এবং ডিডুপ কী meta তে রাখুন। এই অতিরিক্ত কনটেক্সট পরে সাপোর্টের সময় অনেক কাজে লাগে যখন বলা লাগে “এই অর্ডারটিতে কী ঘটেছিল?”\n\n## কীভাবে ইভেন্ট নিরাপদে স্টোর করবেন আপনার বিজনেস রাইটের সাথে\n\nসবচেয়ে গুরুত্বপূর্ণ নিয়ম সহজ: বিজনেস ডেটা ও আউটবক্স ইভেন্ট একই ডাটাবেস ট্রানজ্যাকশনে লিখুন। যদি ট্রানজ্যাকশন কমিট হয়, দুটোই আছে। যদি রোলব্যাক হয়, দুটোই নেই।\n\nউদাহরণ: একজন কাস্টমার অর্ডার দেয়। একটি ট্রানজ্যাকশনে আপনি অর্ডার রো, অর্ডার আইটেম এবং একটি আউটবক্স রো order.created হিসেবে ইনসার্ট করবেন। যদি কোনো ধাপ ব্যর্থ হয়, আপনি চান না যে “created” ইভেন্ট বিশ্বের দিকে পালিয়ে যায়।\n\n### এক ইভেন্ট না অনেক?\n\nযতক্ষণ সম্ভব এক বিজনেস অ্যাকশনের জন্য এক ইভেন্ট দিয়ে শুরু করুন। এটা বোঝা সহজ এবং প্রক্রিয়াজাত করা সস্তা। কেবল তখনই এক ক্লিকে বহু ইভেন্ট জেনারেট করুন যদি ভোক্তারা প্রকৃতপক্ষে আলাদা টাইমিং বা পে-লোড চায় (যেমন order.created ফুলফিলমেন্টের জন্য এবং payment.requested বিলিংয়ের জন্য)। এক ক্লিকের জন্য অনেক ইভেন্ট জেনারেট করলে রিট্রাই, অর্ডারিং জটিলতা, এবং ডুপ্লিকেট হ্যান্ডলিং বেড়ে যায়।\n\n### কী পে-লোড সংরক্ষণ করবেন?\n\nসাধারণত আপনি বেছে নেবেন:\n\n- Snapshot: অ্যাকশনের সময় কী ফিল্ডগুলো ছিল তা সংরক্ষণ (অর্ডার টোটাল, কারেন্সি, কাস্টমার ID)। এটা পরে অতিরিক্ত রিড এড়ায় এবং মেসেজ স্থির রাখে।\n- Reference ID: শুধুমাত্র অর্ডার ID রাখুন এবং ওয়ার্কার পরে ডিটেল লোড করুক। এতে আউটবক্স ছোট থাকে, তবে অতিরিক্ত রিড লাগে এবং অর্ডার সম্পাদিত হলে সেটা বদলে যেতে পারে।\n\nপ্রায়োগিক মধ্যম পথ হল শনাক্তকারী সঙ্গে এক ছোট স্ন্যাপশট রাখা—এটি রিসিভারদের দ্রুত কাজ করতে দেয় এবং ডিবাগিং-এও সাহায্য করে।\n\nট্রানজ্যাকশন বাউন্ডারি সংকীর্ণ রাখুন। একই ট্রানজ্যাকশনের ভিতর তৃতীয় পক্ষের API কল করবেন না।\n\n## তৃতীয় পক্ষের API-তে ইভেন্ট ডেলিভারি: ওয়ার্কার লুপ\n\nএকবার ইভেন্টগুলো আউটবক্সে থাকলে, আপনাকে একটি ওয়ার্কার লাগবে যা সেগুলো পড়ে এবং থার্ড-পার্টি API-কে কল করে। এখান থেকেই প্যাটার্নটি একটি নির্ভরযোগ্য ইন্টিগ্রেশনে পরিণত হয়।\n\nপোলিং সাধারণত সবচেয়ে সহজ অপশন। LISTEN/NOTIFY ল্যাটেন্সি কমাতে পারে, কিন্তু এটি আরও মুভিং পার্টস যোগ করে এবং নোটিফিকেশন মিস হলে বা ওয়ার্কার রিস্টার্ট করলে ফিউলব্যাক দরকার। বেশিরভাগ টিমের জন্য ছোট ব্যাচ সহ স্থির পোলিং চালানো সহজে রাণ ও ডিবাগ করা যায়।\n\n### রো ক্লেইম করা নিরাপদভাবে\n\nওয়ার্কারকে রো ক্লেইম করতে হবে যাতে দুই ওয়ার্কার একই ইভেন্ট একসঙ্গে প্রোসেস না করে। PostgreSQL-এ সাধারণ পদ্ধতি হল রো-লক এবং SKIP LOCKED ব্যবহার করে একটি ব্যাচ সিলেক্ট করা, তারপর সেগুলোকে ইন-প্রোগ্রেস হিসেবে মার্ক করা।\n\nএকটি বাস্তবসম্মত স্ট্যাটাস ফ্লো হতে পারে:\n\n- pending: পাঠানোর জন্য প্রস্তুত\n- processing: ওয়ার্কার দ্বারা লক করা (use locked_by এবং locked_at)\n- sent: সফলভাবে ডেলিভার করা হয়েছে\n- failed: ম্যাক্স অ্যাটেম্পটের পরে থামানো (বা ম্যানুয়াল রিভিউর জন্য সরানো)\n\nবেচাইনগুলো ছোট রাখুন যাতে আপনার ডাটাবেসের প্রতি সদয় থাকা যায়। 10 থেকে 100 রো ব্যাচ, প্রতি 1 থেকে 5 সেকেন্ডে চালানো একটি সাধারণ শুরু পয়েন্ট।\n\nকলে সফল হলে রোকে sent হিসাবে মার্ক করুন। ব্যর্থ হলে attempts বাড়ান, available_at ভবিষ্যতের কোনো সময়ে সেট করুন (ব্যাকঅফ), লক ক্লিয়ার করুন, এবং সেটিকে pending এ ফিরিয়ে দিন।\n\n### লিপি লগিং যা সাহায্য করে (সিক্রেট ফাঁস না করে)\n\nভালো লগ ব্যর্থতাগুলো কার্যকরী করে তোলে। আউটবক্স id, ইভেন্ট টাইপ, ডেস্টিনেশন নাম, অ্যাটেম্পট কাউন্ট, টাইমিং, এবং HTTP স্ট্যাটাস বা এরর ক্লাস লগ করুন। রিকোয়েস্ট বডি, অথ হেডার, এবং পুরো রেসপন্স লগ করা এড়িয়ে চলুন। যদি করিলেশন দরকার হয়, কাঁচা পে-লোডের বদলে একটি সেফ রিকোয়েস্ট ID বা হ্যাশ স্টোর করুন।\n\n## বাস্তব সিস্টেমে কার্যকর অর্ডারিং নিয়ম\n\nঅনেক টিম শুরুতে চায় “ইভেন্টগুলো সেই ক্রমেই পাঠাবো যেভাবে সেগুলো তৈরি করেছি।” সমস্যা হলো “ওই একই ক্রম” গ্লোবাল হওয়া বিরল। যদি আপনি এক গ্লোবাল কিউ জোর করে দেন, একটি ধীর কাস্টমার বা ত্রুটিপূর্ণ API সবার কাজ আটকে দিতে পারে।\n\nএকটি প্রায়োগিক নিয়ম: গোষ্ঠীভিত্তিক অর্ডার বজায় রাখুন, সারা সিস্টেম নয়। এমন একটি গ্রুপিং কী বেছে নিন যা বাইরের দুনিয়া আপনার ডেটাকে কিভাবে দেখে তার সঙ্গে মিলে—যেমন customer_id, account_id, বা aggregate_id (যেমন order_id)। তারপর প্রতিটি গ্রুপের ভিতরে অর্ডারিং নিশ্চিত করুন এবং বিভিন্ন গ্রুপ একসাথে প্যারালালি ডেলিভার করুন।\n\n### প্যারালাল ওয়ার্কার নির্বিঘ্নভাবে অর্ডার ভাঙা ছাড়াই\n\nএকাধিক ওয়ার্কার চালান, কিন্তু নিশ্চিত করুন দুই ওয়ার্কার একই গ্রুপ একই সময়ে প্রসেস না করে। সাধারণ পদ্ধতি হল প্রতিটি aggregate_id এর জন্য সর্বপ্রথম আনসেন্ট ইভেন্টটি ডেলিভার করা এবং ভিন্ন অ্যাগ্রিগেটগুলোর ওপর প্যারালালিজম অনুমোদন করা।\n\nক্লেইমিং নিয়মগুলো সরল রাখুন:\n\n- প্রতিটি গ্রুপের জন্য শুধু প্রায়ই বা সর্বপ্রথম পেন্ডিং ইভেন্ট ডেলিভার করুন।\n- গ্রুপগুলোর মধ্যে প্যারালালিজম দিন, গ্রুপের ভিতরে নয়।\n- একটি ইভেন্ট ক্লেইম করুন, পাঠান, স্ট্যাটাস আপডেট করুন, তারপর এগিয়ে যান।\n\n### যখন এক ইভেন্ট বাকিদের ব্লক করে\n\nএক সময় আসবে যখন একটা “পয়জন” ইভেন্ট কয়েক ঘণ্টা বা দিন ব্যর্থ করবে (খারাপ পে-লোড, টোকেন বাতিল, প্রোভাইডার আউটেজ)। যদি আপনি কঠোরভাবে প্রতি-গ্রুপ অর্ডার বজায় রাখেন, ওই গ্রুপের পরে থাকা ইভেন্টগুলো অপেক্ষা করবে, কিন্তু অন্যান্য গ্রুপ চলতে থাকবে।\n\nএকটি কার্যকর সমঝোতা হল প্রতিটি ইভেন্টের জন্য রিট্রাই সীমা দেওয়া। তা পার হলে সেটিকে failed চিহ্নিত করুন এবং শুধু ঐ গ্রুপটাকেই থামান যতক্ষণ না কেউ মূল কারণ ঠিক করে। এতে একটি ভাঙা কাস্টমার সবার কাজকে ধীর করে না।\n\n## রিট্রাই যাতে পরিস্থিতি খারাপ না করে\n\nরিট্রাই হলো যেখানে একটি ভালো আউটবক্স সেটআপ নির্ভরযোগ্যতা বা আওয়াজ (noisy) উভয় তৈরি করে। লক্ষ্য সহজ: তখনই পুনরায় চেষ্টা করুন যখন সম্ভাবনা বেশি কাজ করবে, এবং যদি কাজ না করে দ্রুত থেমে যান।\n\nএক্সপোনেনশিয়াল ব্যাকঅফ ও একটি হার্ড ক্যাপ ব্যবহার করুন। উদাহরণ: 1 মিনিট, 2 মিনিট, 4 মিনিট, 8 মিনিট, তারপর থামুন (বা সর্বোচ্চ ডিলে 15 মিনিট পর্যন্ত চালিয়ে যান)। সর্বদা সর্বোচ্চ অ্যাটেম্পট সংখ্যা সেট করুন যাতে একটি খারাপ ইভেন্ট সিস্টেম চোক করে রাখতে না পারে।\n\nপ্রতিটি ব্যর্থতা রিট্রাই করা উচিত নয়। নিয়মগুলো পরিষ্কার রাখুন:\n\n- রিট্রাই করবেন: নেটওয়ার্ক টাইমআউট, কানেকশন রিসেট, DNS হিকআপ, এবং HTTP 429 বা 5xx রেসপনস।\n- রিট্রাই করবেন না: HTTP 400 (bad request), 401/403 (অথ সমস্যা), 404 (ভুলো এন্ডপয়েন্ট), বা এমন ভ্যালিডেশন ত্রুটিগুলো যেগুলো প্রেরণের আগে ধরা যায়।\n\nআউটবক্স রো-তে রিট্রাই স্টেট সংরক্ষণ করুন। attempts বাড়ান, পরবর্তী প্রচেষ্টার জন্য available_at সেট করুন, এবং একটি সংক্ষিপ্ত, নিরাপদ এরর সারাংশ রেকর্ড করুন (স্ট্যাটাস কোড, এরর ক্লাস, ট্রিম করা মেসেজ)। এরর ফিল্ডে পূর্ণ পে-লোড বা সংবেদনশীল ডেটা সংরক্ষণ করবেন না।\n\nরেট লিমিটসকে আলাদা ভাবে হ্যান্ডেল করুন। HTTP 429 পেলে Retry-After সম্মান করুন যদি থাকে; নাহলে, রিট্রাই স্টর্ম এড়াতে বেশি আগ্রেসিভ ব্যাকঅফ নিন।\n\n## ডিডুপ্লিকেশন ও আইডেমপোটেন্সি বেসিক্স\n\nযদি আপনি নির্ভরযোগ্য API ইন্টিগ্রেশন বানান, ধরে নিন একই ইভেন্ট দুবারও পাঠানো হতে পারে। ওয়ার্কার HTTP কলের পরে ক্র্যাশ করলে বা টাইমআউট সাফল্য লুকিয়ে রাখলে এ ধরনের পরিস্থিতি হয়। আউটবক্স প্যাটার্ন মিসড ইভেন্ট কমায়, কিন্তু নিজে থেকেই ডুপ্লিকেট প্রতিরোধ করে না।\n\nসবচেয়ে নিরাপদ পদ্ধতি হলো আইডেমপোটেন্সি: একাধিক ডেলিভারি একক ডেলিভারির মতোই ফলাফল দেয়। তৃতীয় পক্ষের API কল করার সময় এমন একটি idempotency কী পাঠান যা ঐ ইভেন্ট ও ঐ ডেস্টিনেশনের জন্য স্থির থাকে। অনেক API হেডার সাপোর্ট করে; না থাকলে কীটি রিকোয়েস্ট বডিতে রাখুন।\n\nএকটি সহজ কী হলো ডেস্টিনেশন প্লাস ইভেন্ট ID। ইভেন্ট ID evt_123 হলে সবসময় ব্যবহার করুন কিছু যেমন destA:evt_123।\n\nআপনার দিক থেকে, ডুপ্লিকেট সেন্ড প্রতিরোধ করতে একটি আউটবাউন্ড ডেলিভারি লগ রাখুন এবং (destination, event_id) মত ইউনিক নিয়ম প্রয়োগ করুন। এমনকি যদি দুই ওয়ার্কার রেস করে, কেবল একজনই “আমরা এটি পাঠাচ্ছি” রেকর্ড তৈরি করতে পারবে।\n\n### ওয়েবহুকগুলিও ডুপ্লিকেট করতে পারে\n\nআপনি যদি ওয়েবহুক কলব্যাক পান (যেমন “ডেলিভারি কনফার্ম” বা “স্ট্যাটাস আপডেট”), সেগুলোতেও একই নিয়ম প্রয়োগ করুন। প্রোভাইডাররা পুনরায় চেষ্টা করে, এবং আপনি একই পে-লোড অনেকবার পেতে পারেন। প্রসেস করা হয়েছে এমন ওয়েবহুক ID সংরক্ষণ করুন, অথবা প্রোভাইডারের মেসেজ ID থেকে একটি স্থির হ্যাশ গণনা করে রেপিটগুলি প্রত্যাখ্যান করুন।\n\n### ডেটা কতদিন রাখবেন\n\nযতক্ষণ না আপনি সফল রেকর্ড করে ফেলছেন (বা একটি চূড়ান্ত ব্যর্থতা গ্রহণ করছেন), আউটবক্স রো রাখুন। ডেলিভারি লগগুলো বেশি সময় রাখুন, কারণ এগুলোই আপনার অডিট ট্রেল যখন কেউ জিজ্ঞাসা করে, “আমরা কি এটা পাঠিয়েছি?”\n\nসাধারণ পদ্ধতি:\n\n- আউটবক্স রো: সফলতার পর ছোট সেফটি উইন্ডোসহ ডিলিট বা আর্কাইভ করুন (কয়েক দিন)।\n- ডেলিভারি লগ: সাপোর্ট ও কমপ্লায়েন্সের প্রয়োজন অনুযায়ী কয়েক সপ্তাহ বা মাস রাখুন।\n- আইডেমপোটেন্সি কী: পর্যাপ্ত সময় রাখুন যতক্ষণ রিট্রাই ঘটতে পারে (ওয়েবহুক ডুপ্লিকেটের জন্য আরও দীর্ঘ)।\n\n## ধাপে ধাপে: আউটবক্স প্যাটার্ন ইমপ্লিমেন্ট করা\n\nনিশ্চিত করুন আপনি কী প্রকাশ করবেন। ইভেন্টগুলো ছোট, ফোকাসড, এবং পরে রিলেতে সহজ হওয়া উচিত। একটি ভাল নিয়ম: প্রতি ইভেন্টে এক ব্যবসায়িক সত্য রাখুন, এবং রিসিভারকে কাজ করার জন্য যথেষ্ট ডেটা দিন।\n\n### ভিত্তি তৈরি করুন\n\nখুলে বোঝার মতো ইভেন্ট নাম রাখুন (উদাহরণ: order.created, order.paid) এবং পে-লোড স্কিমার ভার্সনিং যোগ করুন (যেমন v1, v2)। ভার্সনিং আপনাকে পরে ক্ষেত্র যুক্ত করতে দেয় বাগ ছাড়াই।\n\nআপনার PostgreSQL আউটবক্স টেবিল তৈরি করুন এবং ওয়ার্কার কুয়েরিগুলোর জন্য ইনডেক্স যোগ করুন, বিশেষ করে (status, available_at, id)।\n\nআপনার রাইট ফ্লো আপডেট করুন যেন বিজনেস পরিবর্তন ও আউটবক্স ইনসার্ট একই ট্রানজ্যাকশনে ঘটে—এইটাই মূল গ্যারান্টি।\n\n### ডেলিভারি ও কন্ট্রোল যোগ করুন\n\nসরল বাস্তবায়ন প্ল্যান:\n\n- ইভেন্ট টাইপ ও পে-লোড ভার্সন ডিফাইন করুন যেগুলো আপনি দীর্ঘমেয়াদে সাপোর্ট করতে পারবেন।\n- আউটবক্স টেবিল ও ইনডেক্স তৈরি করুন।\n- মূল ডেটা পরিবর্তনের সাথে সাথেই একটি আউটবক্স রো ইনসার্ট করুন।\n- একটি ওয়ার্কার তৈরি করুন যা রো ক্লেইম করে, থার্ড-পার্টি API-তে সেন্ড করে, এবং স্ট্যাটাস আপডেট করে।\n- ব্যাকঅফ সহ রিট্রাই শেডিউলিং যোগ করুন এবং অ্যাটেম্পট শেষ হলে failed স্টেট ব্যবহার করুন।\n\nবেসিক মেট্রিক্স যোগ করুন যাতে সমস্যা শুরুতেই দেখা যায়: ল্যাগ (সবচেয়ে পুরোনো আনসেন্ট ইভেন্টের আয়ু), পাঠানোর হার, এবং ব্যর্থতার হার।\n\n## একটি সহজ উদাহরণ: অর্ডার ইভেন্ট বাইরের সার্ভিসে পাঠানো\n\nএকজন কাস্টমার আপনার অ্যাপে অর্ডার দেয়। আপনার সিস্টেমের বাইরের দুইটি কাজ ঘটতে হবে: বিলিং প্রোভাইডার কার্ড চার্জ করবে, এবং শিপিং প্রোভাইডার শিপমেন্ট তৈরি করবে।\n\nআউটবক্স প্যাটার্নে, আপনি চেকআউট রিকোয়েস্টের ভিতরে ঐ APIগুলো কল করেন না। বরং আপনি একই PostgreSQL ট্রানজ্যাকশনে অর্ডার ও একটি আউটবক্স ইভেন্ট সংরক্ষণ করেন, তাই আপনি কখনো “অর্ডার সেভ হয়েছে কিন্তু নোটিফিকেশন নেই” (বা উল্টো) অবস্থায় পড়ে থাকবেন না।\n\nএকটি সাধারণ আউটবক্স রো-তে থাকতে পারে aggregate_id (অর্ডার ID), event_type যেমন order.created, এবং আইটেম, টোটাল ও গন্তব্য বিবরণসহ একটি JSONB পে-লোড।\n\nএরপর একটি ওয়ার্কার পেন্ডিং রো তুলে বাইরের সার্ভিসগুলোকে কল করে (বা নির্ধারিত অর্ডারে, বা আলাদা ইভেন্ট payment.requestedshipment.requested ইমিট করে)। এক প্রোভাইডার ডাউন থাকলে ওয়ার্কার অ্যাটেম্পট রেকর্ড করে, পরবর্তী চেষ্টার জন্য available_at বাড়ায়, এবং এগিয়ে চলে। অর্ডার তখনও রয়েছে, এবং ইভেন্ট পরে পুনরায় চেষ্টা করে ব্লক না করে নতুন চেকআউট আটকায় না।\n\nঅর্ডারিং সাধারণত “প্রতি অর্ডার” বা “প্রতি কাস্টমার” হিসেবে করা হয়। একই aggregate_id থাকা ইভেন্টগুলো একবারে একটিই প্রসেস করুন যেন order.paid কখনো order.created-এর আগে না পৌঁছে।\n\nডিডুপ্লিকেশনই আপনাকে দুবার চার্জ হওয়া বা দুটো শিপমেন্ট তৈরি হওয়া থেকে রক্ষা করে। তৃতীয় পক্ষ সমর্থন করলে আইডেমপোটেন্সি কী পাঠান, এবং ডেস্টিনেশন ডেলিভারি রেকর্ড রাখুন যাতে টাইমআউটের পরে রিট্রাই দুইবারের অ্যাকশন ট্রিগার না করে।\n\n## শিপ করার আগে দ্রুত পরীক্ষা করার তালিকা\n\nকোনো ইন্টিগ্রেশনকে টাকা পাঠাতে, কাস্টমার নোটিফাই করতে, বা ডেটা সিঙ্ক করতে দায়ী করার আগে এজ-সেটগুলো পরীক্ষা করুন: ক্র্যাশ, রিট্রাই, ডুপ্লিকেট, এবং একাধিক ওয়ার্কার।\n\nসাধারণ চেকগুলো যা সাধারণ ব্যর্থতা ধরবে:\n\n- নিশ্চিত করুন আউটবক্স রোটি বিজনেস পরিবর্তনের একই ট্রানজ্যাকশনে তৈরি হচ্ছে।\n- যাচাই করুন সেন্ডার একাধিক ইনস্ট্যান্সে নিরাপদে চলতে পারে। দুই ওয়ার্কার একই ইভেন্ট একই সময়ে পাঠানো ঠিক নয়।\n\n- যদি অর্ডারিং গুরুত্বপূর্ণ হয়, এক বাক্যেই নিয়ম সংজ্ঞায়িত করুন এবং একটি স্থিতিশীল কী দিয়ে তা প্রয়োগ করুন।\n\n- প্রতিটি ডেস্টিনেশনের জন্য সিদ্ধান্ত নিন কিভাবে ডুপ্লিকেট প্রতিরোধ করবেন এবং কিভাবে প্রমাণ দেখাবেন “আমরা এটি পাঠিয়েছি।”\n\n- একটি নির্গমন নির্ধারণ করুন: N প্রচেষ্টার পরে ইভেন্টকে failed করুন, শেষ এরর সারাংশ রাখুন, এবং একটি সহজ রি-প্রসেস অ্যাকশন দিন।\n\nবাস্তবতা-চেক: Stripe সম্ভবত অনুরোধ গ্রহণ করতে পারে কিন্তু আপনার ওয়ার্কার সাফল্য রেকর্ড করার আগে ক্র্যাশ করতে পারে। আইডেমপোটেন্সি ছাড়া রিট্রাই ডাবল অ্যাকশন সৃষ্টি করতে পারে। আইডেমপোটেন্সি ও একটি সেভড ডেলিভারি রেকর্ড থাকলে রিট্রাই নিরাপদ হয়।\n\n## পরবর্তী ধাপ: আপনার অ্যাপ অপছন্দ না করে রোলআউট করা\n\nরোলআউট হল যেখানে আউটবক্স প্রকল্পগুলো সাধারণত সফল বা আটকে যায়। শুরুতে ছোট রাখুন যাতে আপনি বাস্তব আচরণ দেখতে পান বিস্তৃত ইন্টিগ্রেশন লেয়ার ঝুঁকিতে না ফেলে।\n\nএকটি ইন্টিগ্রেশন ও এক ইভেন্ট টাইপ দিয়ে শুরু করুন। উদাহরণস্বরূপ, প্রথমে শুধু order.created একটি নির্দিষ্ট ভেন্ডর API-তে পাঠান এবং সবকিছু বাকী রয়ে যাক। এতে আপনি থ্রুপুট, ল্যাটেন্সি ও ব্যর্থতার হার সম্পর্কে পরিষ্কার বেসলাইন পাবেন।\n\nসমস্যাগুলো শুরুতেই দৃশ্যমান করুন। আউটবক্স ল্যাগ (কত ইভেন্ট অপেক্ষায় আছে, এবং সবচেয়ে পুরনোটি কতদিনের) এবং ব্যর্থতার হারের জন্য ড্যাশবোর্ড ও অ্যালার্ট যোগ করুন; যদি 10 সেকেন্ডে আপনি উত্তর দিতে পারেন “আমরা কি পিছিয়ে?”, তাহলে ব্যবহারকারীরা সমস্যার আগে ধরে ফেলতে পারবেন।\n\nপ্রথম ইনসিডেন্টের আগে একটি নিরাপদ রি-প্রসেস প্ল্যান রাখুন। নির্ধারণ করুন “রি-প্রসেস” মানে কি: একই পে-লোড পুনরায় চেষ্টা করা, বর্তমান ডেটা থেকে পে-লোড পুনর্নির্মাণ, না কি ম্যানুয়াল রিভিউ করা। ডকুমেন্ট করুন কোন ক্ষেত্রে পুনরায় পাঠানো নিরাপদ এবং কোনগুলোতে মানব-পরীক্ষা দরকার।\n\nআপনি যদি AppMaster (appmaster.io) মত একটি নো-কোড প্ল্যাটফর্ম ব্যবহার করে এটি তৈরি করেন, তখন একই স্ট্রাকচার প্রযোজ্য: PostgreSQL-এ আপনার বিজনেস ডেটা এবং একটি আউটবক্স রো একসঙ্গে লিখুন, তারপর একটি আলাদা ব্যাকএন্ড প্রসেস চালান ডেলিভারি, রিট্রাই, এবং ইভেন্টগুলোকে sent বা failed মার্ক করার জন্য।

প্রশ্নোত্তর

When should I use the outbox pattern instead of calling the API directly?

ব্যবহারকারীর কোনো কাজ আপনার ডাটাবেস আপডেট করে এবং অন্য সিস্টেমে কাজ-trigger করে তখন আউটবক্স প্যাটার্ন ব্যবহার করুন। এটা বিশেষভাবে কাজে আসে যখন টাইমআউট, অনিয়মিত নেটওয়ার্ক বা তৃতীয় পক্ষের আউটেজের জন্য “আমাদের সিস্টেমে সেভ হয়েছে, তাদের সিস্টেমে নেই” ধরনের সমস্যা দেখা দিতে পারে।

Why does the outbox insert need to be in the same transaction as the business write?

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

What fields should an outbox table include to be practical?

একটি ব্যবহারযোগ্য ডিফল্টের জন্য id, aggregate_id, event_type, payload, status, created_at, available_at, attempts, এবং লক ফিল্ডগুলো যেমন locked_atlocked_by রাখুন। এতে সেন্ডিং, রিট্রাই শেডিউলিং, এবং নিরাপদ কনকারেন্সি সহজ থাকে।

What indexes matter most for an outbox table in PostgreSQL?

সাধারণত (status, available_at, id) এর উপর একটি ইনডেক্স সবচেয়ে প্রয়োজনীয়, যাতে ওয়ার্কার দ্রুত পরবর্তী পাঠযোগ্য ইভেন্টগুলো সময়ক্রমে পেলে। অতিরিক্ত ইনডেক্স শুধুমাত্র তখন যোগ করুন যখন আপনি সত্যিই ঐ ফিল্ডগুলোর উপর কুয়েরি চালান—অতিরিক্ত ইনডেক্স ইনসার্ট ধীর করে।

Should my worker poll the outbox table or use LISTEN/NOTIFY?

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

How do I prevent two workers from sending the same outbox event?

রো-লেভেল লক ব্যবহার করে রো ক্লেইম করুন যাতে দুইটি ওয়ার্কার একই ইভেন্ট একই সময়ে প্রসেস করতে না পারে—সাধারণত SKIP LOCKED ব্যবহার করা হয়। তারপর রোকে processing হিসাবে মার্ক করুন, locked_at ও ওয়ার্কার আইডি রাখুন, পাঠান, এবং শেষে sent বা ভবিষ্যতের available_at দিয়ে pending এ ফিরিয়ে দিন।

What’s the safest retry strategy for outbox deliveries?

এক্সপোনেনশিয়াল ব্যাকঅফ এবং সর্বোচ্চ অ্যাটেম্পট সেট করুন, এবং কেবল মোটামুটি আর ফিরে আসার মতো ত্রুটিগুলোই রিট্রাই করুন। টাইমআউট, নেটওয়ার্কের ত্রুটি, এবং HTTP 429/5xx ভালো রিট্রাই কেস; ভ্যালিডেশন ত্রুটি বা বেশিরভাগ 4xx রেসপনস সাধারণত চূড়ান্ত বলে বিবেচনা করুন যতক্ষণ না আপনি ডেটা বা কনফিগ ঠিক করেন।

Does the outbox pattern guarantee exactly-once delivery?

আউটবক্স প্যাটার্নটি এখনও ডুপ্লিকেট সম্পূর্ণরূপে প্রতিরোধ করে না—এক্সট্রা ডেলিভারি ঘটতে পারে যদি ওয়ার্কার HTTP কলের পর ক্র্যাশ করে কিন্তু সাফল্য রেকর্ড না করে। তাই আইডেমপোটেন্সি কীগুলো ব্যবহার করুন যা প্রতিটি গন্তব্য ও ইভেন্টের জন্য স্থির থাকে, এবং একটি ডেলিভারি লগ রাখুন যাতে ইউনিক কনসট্রেইন্ট (যেমন (destination, event_id)) দিয়ে রেস কন্ডিশন ঠেকানো যায়।

How do I handle ordering without slowing down the whole system?

গোষ্ঠীভিত্তিকভাবে অর্ডার বজায় রাখুন, সমগ্র সিস্টেম নয়। aggregate_id (অর্ডার ID) বা customer_id মতো গ্রুপিং কী ব্যবহার করুন, একই গ্রুপে এক সময়ে কেবল একটি ইভেন্ট প্রসেস করুন, এবং বিভিন্ন গ্রুপ পারালালভাবে প্রসেস করতে দিন—এভাবে একজন ধীর গ্রাহক সবার গতিটা ধীর করবে না।

What should I do with a “poison” event that keeps failing?

একটি ইভেন্ট দীর্ঘ সময় ধরে ব্যর্থ করলে সেটিকে সর্বোচ্চ প্রচেষ্টার পরে failed হিসেবে চিহ্নিত করুন, সংক্ষিপ্ত নিরাপদ এরর সারাংশ রাখুন, এবং একই গ্রুপের পরে থাকা ইভেন্টগুলো তখন পর্যন্ত থামিয়ে রাখুন যতক্ষণ না কেউ মূল সমস্যা ঠিক করে। এতে শুধুমাত্র প্রভাবিত গ্রুপই বন্ধ থাকবে এবং বাকিরা চলতে থাকবে।

শুরু করা সহজ
কিছু আশ্চর্যজনকতৈরি করুন

বিনামূল্যের পরিকল্পনা সহ অ্যাপমাস্টারের সাথে পরীক্ষা করুন।
আপনি যখন প্রস্তুত হবেন তখন আপনি সঠিক সদস্যতা বেছে নিতে পারেন৷

এবার শুরু করা যাক