সমান্তরাল-নিরাপদ ইনভয়েস নম্বরিং যা ডুপ্লিকেট ও গ্যাপ এড়ায়
বহু ব্যবহারকারী একই সাথে ইনভয়েস বা টিকেট তৈরি করলে ডুপ্লিকেট ও অনাকাঙ্ক্ষিত গ্যাপ এড়াতে প্র্যাকটিক্যাল প্যাটার্ন শিখুন।

যখন একসাথে দুইজন রেকর্ড তৈরি করে তখন কী ঘটে
ধরুন বিকেলে 4:55 মিনিট, অফিসে চাপ আছে। দুইজনই ইনভয়েস শেষ করলেন এবং এক সেকেন্ডের মধ্যে Save চাপলেন। দুইজনের স্ক্রিনেও সাময়িকভাবে "Invoice #1042" দেখা গেল। একটা রেকর্ড জিতে যায়, অন্যটা ব্যর্থ হয়, অথবা সবচেয়ে খারাপ—দুটোই একই নম্বর নিয়ে সেভ হয়ে যায়। বাস্তবে সবচেয়ে বেশি দেখা সমস্যা হলো: লোডে গিয়ে প্রদর্শিত হওয়া ডুপ্লিকেট নম্বর।
টিকেটগুলিও একইভাবে আচরণ করে। দুই এজেন্ট একই সময়ে একই গ্রাহকের জন্য নতুন টিকেট তৈরি করলে, এবং যদি আপনার সিস্টেম "পরবর্তী নম্বর নাও" দেখে সেটাকে ব্যবহার করে, তাহলে উভয় অনুরোধই একই "সর্বশেষ" মান পড়তে পারে এবং একই পরবর্তী নম্বর বেছে নিতে পারে।
দ্বিতীয় সমস্যা একটু সূক্ষ্ম: নম্বর স্কিপ হওয়া। আপনি দেখতে পারেন #1042, তারপর #1044—#1043 মিসিং। এটি সাধারণত কোনো ত্রুটি বা রিট্রাইয়ের পরে হয়। একটি অনুরোধ নম্বর রিজার্ভ করে, তারপর সেভ ব্যর্থ হয় ভ্যালিডেশন ত্রুটি, টাইমআউট, বা ইউজার ট্যাব বন্ধ করার কারণে। অথবা ব্যাকগ্রাউন্ড জব নেটওয়ার্ক সমস্যা থেকে রিটি করতে এসে নতুন নম্বর নেয়, অথচ প্রথম প্রচেষ্টা ইতিমধ্যে একটি নম্বর কনজিউম করেছে।
ইনভয়েসের ক্ষেত্রে এটি গুরুত্বপূর্ণ কারণ নম্বরিং আপনার অডিট ট্রেইলের অংশ। একাউন্ট্যান্টরা প্রত্যাশা করেন প্রতিটি ইনভয়েস অনন্যভাবে শনাক্তযোগ্য হবে, এবং গ্রাহকরা পেমেন্ট বা সাপোর্ট ইমেলে ইনভয়েস নম্বর রেফার করতে পারেন। টিকেটের ক্ষেত্রে নম্বরই হলো কথোপকথন, রিপোর্ট, এক্সপোর্টের হাতল। ডুপ্লিকেটগুলো বিভ্রান্তি সৃষ্টি করে। মিসিং নম্বরগুলো রিভিউতে প্রশ্ন তুলতে পারে, যদিও কিছু অসৎ ঘটনা না ঘটলেও।
প্রাথমিকভাবে একটা মূল প্রত্যাশা নির্ধারণ করা জরুরি: প্রতিটি নম্বর পদ্ধতি একই সঙ্গে সমান্তরাল-নিরাপদ এবং gapless হতে পারে না। সমান্তরাল-নিরাপদ ইনভয়েস নম্বরিং (অনেক ব্যবহারকারী থাকলেও ডুপ্লিকেট নয়) অর্জনযোগ্য এবং অপ্রতিরোধ্য হওয়া উচিত। gapless নম্বরিংও সম্ভব, কিন্তু তা অতিরিক্ত নিয়ম ও প্রায়ই ড্রাফট, ব্যর্থতা, এবং বাতিলকরণ কিভাবে হ্যান্ডল করবেন তা বদলে দেয়।
সমস্যাটি ভালোভাবে বুঝতে একটি উপায় হলো প্রশ্ন করা: আপনার নম্বরগুলো কোন নিশ্চয়তা দেবে—
- কখনোই পুনরাবৃত্তি হবে না (চিরতরে ইউনিক)
- বেশিরভাগ ক্ষেত্রে বাড়তে থাকবে (ভালো তো)
- কখনোই স্কিপ হবে না (শুধুমাত্র আপনি যদি সেটার জন্য ডিজাইন করেন)
একবার নিয়ম নির্বাচন করলে, টেকনিক্যাল সলিউশন বেছে নেয়া সহজ হয়।
কেন ডুপ্লিকেট ও গ্যাপ ঘটে
অধিকাংশ অ্যাপ একটি সাধারণ প্যাটার্ন অনুসরণ করে: ইউজার Save ক্লিক করে, অ্যাপ "পরবর্তী ইনভয়েস/টিকেট নম্বর" খোঁজে, তারপর সেই নম্বর সহ নতুন রেকর্ড ইনসার্ট করে। এক জন ব্যবহারকারী থাকতে এটা নিরাপদ মনে হয় কারণ একাই করলে ঠিক কাজ করে।
সমস্যা শুরু হয় যখন দুইটি সেভ প্রায় একই সময়ে ঘটে। উভয় অনুরোধই "পরবর্তী নম্বর নাও" ধাপটি পৌঁছাতে পারে আগে কোনোটি ইনসার্ট শেষ করে। যদি উভয় পড়ে একই "পরবর্তী" মান দেখেন, তারা উভয়েই একই নম্বর লেখার চেষ্টা করবে। এটাই রেস কন্ডিশন: ফলাফল টাইমিংয়ের উপর নির্ভর করে, লজিকের উপর নয়।
সাধারণ টাইমলাইন এমন হতে পারে:
- অনুরোধ A পরবর্তী নম্বর পড়ে: 1042
- অনুরোধ B পরবর্তী নম্বর পড়ে: 1042
- অনুরোধ A ইনভয়েস 1042 ইনসার্ট করে
- অনুরোধ B ইনভয়েস 1042 ইনসার্ট করে (বা unique নিয়ম থাকলে ব্যর্থ হয়)
ডুপ্লিকেট তখনই ঘটে যখন ডাটাবেসে কিছুই দ্বিতীয় ইনসার্টকে বাধা দেয় না। যদি আপনি শুধু অ্যাপ কোডে "এই নম্বর নেওয়া আছে কিনা" চেক করেন, চেক ও ইনসার্টের মধ্যে রেস হারাতে পারেন।
গ্যাপগুলো আলাদা সমস্যা। যখন সিস্টেম একটি নম্বর "রিজার্ভ" করে কিন্তু রেকর্ড কখনই বাস্তব, কমিট করা ইনভয়েস/টিকেট হয় না, তখন গ্যাপ হয়। সাধারণ কারণ: ব্যর্থ পেমেন্ট, পরে পাওয়া ভ্যালিডেশন ত্রুটি, টাইমআউট, বা ইউজার ট্যাব বন্ধ করে ফেলা। এমনকি ইনসার্ট ব্যর্থ হলে ও কিছু সেভ না হলেও, নম্বরটি ইতিমধ্যে খরচ হয়ে যেতে পারে।
লুকানো কনকরেন্সি এটাকে আরও খারাপ করে, কারণ এটা রোজরোজই কেবল "দুই মানুষ ক্লিক করা" নয়—আপনার কাছে থাকতে পারে:
- API ক্লায়েন্ট যারা সমান্তরালে রেকর্ড তৈরি করে
- ব্যাচে রান করা ইমপোর্টস
- রাতের বেলার ব্যাকগ্রাউন্ড জবগুলো
- দুষ্টুমিপূর্ণ কানেকশনের কারণে মোবাইল অ্যাপের রিট্রাই
সুতরাং মূল কারণগুলো হলো: (1) বহু অনুরোধ যখন একই কাউন্টার ভ্যালু পড়ে তখন টাইমিং সংঘাত, এবং (2) নম্বর বরাদ্দ করা হয় আগে আপনি নিশ্চিত হন ট্রানজ্যাকশন সফল হবে কিনা। যে কোনও প্ল্যান বানাতে হবে এটা বিবেচনা করে: কোন ফলাফল আপনি সহন করবেন—ডুপ্লিকেট নেই, গ্যাপ নেই, বা দুটোই, এবং কোন ইভেন্টে (ড্রাফট, রিট্রাই, ক্যানসেল) এই নিয়ম প্রযোজ্য হবে।
একটি সমাধান বেছে নেওয়ার আগে আপনার নম্বরিং নীতি নির্ধারণ করুন
সমান্তরাল-নিরাপদ ইনভয়েস নম্বরিং ডিজাইন করার আগে লিখে রাখুন নম্বর ব্যবসায়ে কী মানে রাখে। সবচেয়ে সাধারণ ভুল হলো প্রথমে টেকনিক্যাল পদ্ধতি বেছে নেওয়া, পরে বুঝা যে একাউন্টিং বা আইনগত নিয়ম ভিন্ন প্রত্যাশা করে।
শুরু করুন দুটি লক্ষ্য আলাদা করে দেখায় যা মিশে যায়:
- ইউনিক: দুইটি ইনভয়েস বা টিকেট কখনো একই নম্বর ভাগ করে না।
- Gapless: নম্বর ইউনিক এবং সম্পূর্ণ কনসেকিউটিভ (কোনো মিসিং নেই)।
অনেক বাস্তব সিস্টেমে শুধু ইউনিক লক্ষ্যে এগোতে চান এবং গ্যাপ মেনে নেয়া হয়। গ্যাপ সাধারণত স্বাভাবিক: একটি ইউজার ড্রাফট খুলে রেখে চলে গেলে, পেমেন্ট ব্যর্থ হলে, বা রেকর্ড তৈরি করে পরে void করলে। হেল্পডেস্ক টিকেটে গ্যাপগুলো প্রায়ই তেমন সমস্যা করে না। ইনভয়েসের ক্ষেত্রেই অনেক দল গ্যাপ গ্রহণ করে যদি তারা অডিট ট্রেইলে ব্যাখ্যা দিতে পারে (voided, canceled, test ইত্যাদি)। Gapless নম্বরিং সম্ভব, তবে এটা অতিরিক্ত নিয়ম চাপায় এবং প্রায়ই কাজে ধীরগতি আনে।
পরবর্তী ধাপে, কাউন্টারটির স্কোপ ঠিক করুন। ছোট শব্দচয়নও ডিজাইন বদলে দেয়:
- সবকিছুর জন্য একটা গ্লোবাল সিকোয়েন্স, নাকি আলাদা সিকোয়েন্স প্রতি কোম্পানি/টেন্যান্টের জন্য?
- প্রতি বছর রিসেট হবে (2026-000123) নাকি কখনো রিসেট হবে না?
- ইনভয়েস বনাম ক্রেডিট নোট বনাম টিকেট—শুধু আলাদা সিরিজ?
- আপনি কি হিউম্যান-ফ্রেন্ডলি ফরম্যাট চান (প্রিফিক্স, সেপারেটর), নাকি শুধু ভিতরের নম্বর?
কনক্রিট উদাহরণ: একটি SaaS প্রোডাক্টে অনেক ক্লায়েন্ট কোম্পানি থাকতে পারে; তারা চাইতে পারে ইনভয়েস নম্বর কোম্পানি-স্তরে ইউনিক এবং প্রতি ক্যালেন্ডার বছরে রিসেট হয়, কিন্তু টিকেটগুলো গ্লোবালি ইউনিক এবং কখনো রিসেট হয় না। এগুলো দুইটি আলাদা কাউন্টার যা ডিজাইনকে আলাদা করবে, যদিও UI একই দেখাতে পারে।
আপনি যদি সত্যিই gapless প্রয়োজন, স্পষ্ট করে লিখে রাখুন নম্বর বরাদ্দের পরে কী ইভেন্টগুলো অনুমোদিত। উদাহরণ: একটি ইনভয়েস কি ডিলিট করা যাবে, নাকি কেবল বাতিল করা যাবে? ইউজাররা কি ড্রাফট সেভ করতে পারবে নম্বর ছাড়া এবং শুধু ফাইনাল অনুমোদনের সময় নম্বর পাবে? এই সিদ্ধান্তগুলো প্রায়ই DB কৌশলের চাইতে বেশি প্রভাব ফেলে।
সংক্ষেপে লিখে রাখুন:
- কোন রেকর্ড টাইপ সিকোয়েন্স ব্যবহার করে?
- কখন একটি নম্বরকে “ব্যবহৃত” ধরা হবে (ড্রাফট, পাঠানো, পেইড)?
- স্কোপ কী (গ্লোবাল, প্রতি কোম্পানি, প্রতি বছর, প্রতি সিরিজ)?
- ভয়েড এবং করেকশান কিভাবে হ্যান্ডল করবেন?
AppMaster-এ এই ধরনের নীতি আপনার ডেটা মডেল ও বিজনেস প্রসেস ফ্লো-র পাশে রাখা ভালো, যাতে টিম একই আচরণ API, ওয়েব UI ও মোবাইল-এ অনুসরণ করে এবং বিস্ময় না ঘটে।
সাধারণ পদ্ধতি ও প্রতিটি পদ্ধতি কী নিশ্চয়তা দেয়
লোকেরা যখন “ইনভয়েস নম্বরিং” বলে, তারা প্রায়ই দুটি আলাদা লক্ষ্য মিশিয়ে দেয়: (1) একই নম্বর দুবার জেনারেট করা যাবে না, এবং (2) কোনো গ্যাপ থাকবে না। বেশিরভাগ সিস্টেম প্রথমটা সহজে নিশ্চিত করতে পারে। দ্বিতীয়টা অনেক কঠিন, কারণ গ্যাপ তখনই দেখা যায় যখন ট্রানজ্যাকশন ব্যর্থ হয়, ড্রাফট পরিত্যাগ করা হয়, বা রেকর্ড void করা হয়।
পদ্ধতি 1: ডাটাবেস sequence (দ্রুত ইউনিকনেস)
PostgreSQL sequence হলো সবচেয়ে সহজ উপায় ইউনিক, ক্রমবর্ধমান নম্বর পেতে। এটি ভালো স্কেলে কাজ করে কারণ ডাটাবেস দ্রুত সিকোয়েন্স ভ্যালু হস্তান্তর করতে পারে, এমনকি অনেক ব্যবহারকারী একই সঙ্গে থাকলেও।
আপনি কী পাবেন: ইউনিকনেস ও অর্ডারিং (প্রায়শই বাড়ে)। আপনি কি পাবেন না: gapless নম্বর। যদি ইনসার্ট ব্যর্থ হয় সিকোয়েন্স ভ্যালু "বর্ণিত" হয়ে যায় এবং আপনি গ্যাপ দেখবেন।
পদ্ধতি 2: ইউনিক কনস্ট্রেইন্ট + রিট্রাই (ডেটাবেসকে সিদ্ধান্ত নিতে দিন)
এখানে আপনি অ্যাপ লজিক থেকে একটি ক্যান্ডিডেট নম্বর জেনারেট করেন, সেভ করেন, এবং ডুপ্লিকেট হলে UNIQUE কনস্ট্রেইন্ট রিপ্রোজেক্ট করে। কনফ্লিক্ট হলে আপনি রিট্রাই করেন।
এটি কাজ করতে পারে, কিন্তু উচ্চ কনকরেন্সিতে এটি বেশি রিট্রাই ও ব্যর্থ ট্রানজ্যাকশনের কারণ হয়ে ওঠে এবং ডিবাগ করতে জটিলতা বাড়ে। এটি gapless নম্বর নিশ্চিত করে না, যতক্ষণ না আপনি কড়া রিজার্ভেশন নিয়ম যোগ করেন, যা জটিলতা বাড়ায়।
পদ্ধতি 3: কাউন্টার সারি সহ লকিং (gapless লক্ষ্য)
আপনি যদি সত্যিই gapless নম্বর চান, সাধারণ প্যাটার্ন হলো একটি ডেডিকেটেড কাউন্টার টেবিল (প্রতিটি স্কোপের জন্য এক সারি, যেমন প্রতি বছর বা প্রতি কোম্পানি)। ট্রানজ্যাকশনে ওই সারিটি লক করে, এটি ইনক্রিমেন্ট করে এবং নতুন মান ব্যবহার করা হয়।
এটি gapless-র সবচেয়ে কাছাকাছি, কিন্তু খরচ আছে: এটি একটি হট স্পট তৈরি করে যেখানে সব রাইটারকে অপেক্ষা করতে হয়। অপারেশনাল ভুল (দীর্ঘ ট্রানজ্যাকশন, টাইমআউট, ডেডলক) এই ক্ষেত্রে ঝুঁকি বাড়ায়।
পদ্ধতি 4: আলাদা রিজার্ভেশন সার্ভিস (বিশেষ ক্ষেত্রের জন্য)
একটি স্বতন্ত্র "নাম্বারিং সার্ভিস" বহু অ্যাপ বা ডাটাবেস জুড়ে নিয়ম কেন্দ্রীয়করণ করতে পারে। সাধারণত এটি তখনই মূল্যবান হয় যখন কয়েকটি সিস্টেম নম্বর ইস্যু করে এবং আপনি লেখাগুলো একত্রিত করতে পারবেন না।
ট্রেডঅফ হলো অপারেশনাল ঝুঁকি: আপনি আরেকটি সার্ভিস যোগ করেছেন যা সঠিক, উচ্চ-উপলব্ধ এবং কনসিসটেন্ট হতে হবে।
সংক্ষিপ্তভাবে গ্যারান্টি ভাবলে:
- Sequence: ইউনিক, দ্রুত, গ্যাপ গ্রহণ করে
- Unique + retry: ইউনিক, কম লোডে সহজ, উচ্চ লোডে থ্রাশ করতে পারে
- Locked counter row: gapless হতে পারে, উচ্চ কনকরেন্সিতে ধীর
- Separate service: সিস্টেম জুড়ে নমনীয়, সবচেয়ে জটিল ও ঝুঁকিপূর্ণ
আপনি যদি AppMaster-এ এটি তৈরি করেন, একই পছন্দ প্রযোজ্য: ডাটাবেসই যেখানে কোর কনসিস্টেন্সি থাকা উচিত। অ্যাপ লজিক রিট্রাই ও স্পষ্ট এরর মেসেজে সহায়তা করতে পারে, কিন্তু চূড়ান্ত গ্যারান্টি constraint ও ট্রানজ্যাকশনের কাছ থেকে আসা উচিত।
ধাপে ধাপে: sequence ও unique constraint দিয়ে ডুপ্লিকেট প্রতিরোধ করুন
আপনি যদি ডুপ্লিকেট প্রতিরোধ করাই মূল লক্ষ্য (গ্যাপ না থাকা নয়), তাহলে সবচেয়ে সহজ ও নির্ভরযোগ্য প্যাটার্ন হলো: ডাটাবেস-জেনারেটেড ইন্টারনাল ID ব্যবহার করুন এবং দেখানোর জন্য আলাদা ফিল্ডে ইউনিকনেস বজায় রাখুন।
প্রথমেই দুটি ধারণা আলাদা করুন। জয়েন, এডিট, এক্সপোর্টের জন্য ডাটাবেস-জেনারেটেড মান (identity/sequence) ব্যবহার করুন। invoice_no বা ticket_no আলাদা কলাম রাখুন যা মানুষের সামনে দেখানো হয়।
PostgreSQL-এ একটি বাস্তবানুগ সেটআপ
নিচে একটি সাধারণ PostgreSQL পদ্ধতি আছে যা ডিসপ্লে নম্বর জেনারেশন ডাটাবেসের ভিতরেই রাখে, যেখানে কনকরেন্সি সঠিকভাবে হ্যান্ডল হয়।
-- Internal, never-shown primary key
create table invoices (
id bigint generated always as identity primary key,
invoice_no text not null,
created_at timestamptz not null default now()
);
-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);
-- Sequence for the visible number
create sequence invoice_no_seq;
এখন ইনসার্ট সময় ডিসপ্লে নম্বর জেনারেট করুন ("select max(invoice_no) + 1" না করে)। একটি সাধারণ প্যাটার্ন হলো INSERT-এই sequence ভ্যালু ফরম্যাট করা:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
50 জন ইউজার একই সময়ে "Create invoice" চাপলেও, প্রতিটি ইনসার্ট আলাদা sequence ভ্যালু পাবে এবং unique index কোনো দুর্ঘটনাজনিত ডুপ্লিকেট ব্লক করবে।
কলোশন হলে কী করবেন
একটি সাধারণ sequence-এ কলোশন বিরল। সাধারণত এটি তখনই ঘটে যখন আপনি অতিরিক্ত নিয়ম যোগ করেন যেমন "প্রতি বছর রিসেট" বা "প্রতি টেন্যান্ট", অথবা ইউজার-এডিটেবল নম্বর। সেজন্য unique কনস্ট্রেইন্ট থাকা জরুরি।
অ্যাপ লেভেলে unique-violation এড়াতে ছোট একটি রিট্রাই লুপ রাখুন:
- ইনসার্ট চেষ্টা করুন
- যদি invoice_no-তে unique constraint error পান, আবার চেষ্টা করুন
- কয়েকবার চেষ্টা করে না পারলে পরিষ্কার ত্রুটি দেখান
এটি ভাল কাজ করে কারণ রিট্রাই তখনই ট্রিগার হয় যখন কিছু অস্বাভাবিক ঘটে—যেমন দুই আলাদা কোড পাথ একই ফরম্যাট করা নম্বর তৈরি করে।
রেস উইন্ডো ছোট রাখুন
নাম্বার UI-তে কনট্যাক্ট করে গণনা করবেন না, এবং "রিজার্ভ" করতে আগে পড়বেন না। সংখ্যাটি যতটা সম্ভব ডাটাবেস লেখার কাছাকাছি জেনারেট করুন।
AppMaster-এ PostgreSQL ব্যবহার করলে, আপনি Data Designer-এ id কে identity primary key হিসেবে মডেল করতে পারেন, invoice_no-তে unique constraint যোগ করতে পারেন, এবং create ফ্লোতে invoice_no জেনারেট করতে পারেন যাতে সেটি ইনসার্টের সাথে একসাথে ঘটে। এইভাবে ডাটাবেস সোর্স অফ ট্রুথ থাকে এবং কনকরেন্সি PostgreSQL-ই ভালভাবে হ্যান্ডল করে।
ধাপে ধাপে: row locking দিয়ে gapless কাউন্টার তৈরি করুন
আপনি যদি সত্যিই gapless নম্বর চান (কোনো মিসিং নম্বর নয়), তাহলে ট্রানজ্যাকশনাল কাউন্টার টেবিল ও সারি লকিং ব্যবহার করতে পারেন। ধারণাটা সহজ: একই সময়ে কেবল একটি ট্রানজ্যাকশনই নির্দিষ্ট স্কোপের পরবর্তী নম্বর নিতে পারে, তাই নম্বর অর্ডারে হস্তান্তরিত হয়।
প্রথমে স্কোপ ঠিক করুন। অনেক টিম আলাদা সিকোয়েন্স চান প্রতি কোম্পানি, প্রতি বছর, বা প্রতি সিরিজ অনুযায়ী। কাউন্টার টেবিল শেষ ব্যবহৃত নম্বর সঞ্চয় করে প্রতিটি স্কোপের জন্য।
PostgreSQL row locks ব্যবহার করে বাস্তবপোথিক প্যাটার্ন:
number_countersনামে একটি টেবিল তৈরি করুন, যেখানে কলাম থাকবেcompany_id,year,series,last_number, এবং একটি unique key(company_id, year, series)।- একটি ডাটাবেস ট্রানজ্যাকশন শুরু করুন।
- আপনার স্কোপের জন্য কাউন্টার সারি লক করুন:
SELECT last_number FROM number_counters WHERE ... FOR UPDATE। next_number = last_number + 1গণনা করুন, কাউন্টার সারি আপডেট করেlast_number = next_numberরাখুন।- সেই
next_numberব্যবহার করে ইনভয়েস/টিকেট ইনসার্ট করুন, তারপর কমিট করুন।
মূল কথা হলো FOR UPDATE। লোডে, ডুপ্লিকেট পাওয়া যাবে না। আপনি "দুটি ইউজার একই নম্বর পেয়েছিল" থেকে গ্যাপও পাবেন না, কারণ দ্বিতীয় ট্রানজ্যাকশন প্রথমটি কমিট বা রোলব্যাক না করা পর্যন্ত একই কাউন্টার সারি পড়তে ও ইনক্রিমেন্ট করতে পারে না। পরিবর্তে, দ্বিতীয় অনুরোধ সাময়িকভাবে অপেক্ষা করবে। ঐ অপেক্ষাটাই gapless হওয়ার মূল্য।
নতুন স্কোপ ইনিশিয়ালাইজ করা
নতুন কোম্পানি, নতুন বছর বা নতুন সিরিজ আসে—তার জন্য পরিকল্পনা দরকার:
- আগাম কাউন্টার সারি প্রি-ক্রিয়েট করুন (উদাহরণ: ডিসেম্বরেই পরের বছরের সারি তৈরি করুন)।
- অন-ডিমান্ডে সারি তৈরি করুন: চেষ্টা করে insert করুন
last_number = 0দিয়ে, যদি ইতিমধ্যে থাকে তাহলে স্বাভাবিক lock-and-increment ফ্লোতে ফিরুন।
AppMaster-এ এটি তৈরি করলে পুরো "লক, ইনক্রিমেন্ট, ইনসার্ট" সিকোয়েন্স একটিকেই ট্রানজ্যাকশনে রাখুন, যাতে সবকিছু বা কিছুই না ঘটে।
এজ কেসসমূহ: ড্রাফট, ব্যর্থ সেভ, বাতিলকরণ ও এডিট
অধিকাংশ নম্বরিং বাগ জারিত হয় ঠিক কী messy অংশগুলিতে: ড্রাফট যা কখনই পোস্ট হয় না, সেভ যা ব্যর্থ হয়, ইনভয়েস বাতিল বা রেকর্ড এডিট যখন কেউ নম্বর আগে দেখে ফেলে। যদি আপনি সমান্তরাল-নিরাপদ ইনভয়েস নম্বরিং চান, তখন নম্বর কখন "রিয়েল" হবে সে বিষয়ে স্পষ্ট নিয়ম থাকা দরকার।
সবচেয়ে বড় সিদ্ধান্ত হলো টাইমিং। যদি আপনি কেউ "New invoice" ক্লিক করলেই নম্বর দেন, তাহলে আপনি পরিত্যক্ত ড্রাফট থেকে গ্যাপ পাবেন। যদি আপনি কেবলমাত্র যখন ইনভয়েস ফাইনালাইজ করা হয় (posted, issued, sent অথবা আপনার ব্যবসার মানে যেটা) তখন নম্বর দেন, তাহলে নম্বরগুলো টাইট হয়ে থাকে এবং ব্যাখ্যা করাও সহজ।
ব্যর্থ সেভ ও রোলব্যাক হল যেখানে প্রত্যাশা প্রায়ই ডাটাবেস আচরণের সাথে সংঘর্ষ করে। একটি সাধারণ sequence-এ একবার নম্বর নেওয়া হলে সেটি নেওয়া বলে ধরা হয়, এমনকি ট্রানজ্যাকশন পরে ব্যর্থ হলে ও। এটা স্বাভাবিক ও নিরাপদ, কিন্তু গ্যাপ তৈরি করতে পারে। যদি আপনার নীতি gapless চায়, নম্বর অবশ্যই কেবল চূড়ান্ত ধাপে ও ট্রানজ্যাকশন কমিট হওয়ার সময়ই বরাদ্দ করতে হবে। সাধারণত এর মানে হলো একটি একক কাউন্টার সারি লক করে শেষ নম্বর লেখা, তারপর কমিট—যদি কোনো ধাপ ব্যর্থ হয়, কিছুই বরাদ্দ হবে না।
ক্যানসেল ও ভয়েড করা রেকর্ড কখনোই নম্বর পুনরায় ব্যবহার করা উচিত নয়। নম্বর অক্ষত রাখুন এবং স্ট্যাটাস পরিবর্তন করুন। অডিটর ও গ্রাহকরা ইতিহাস অক্ষত রাখার আশা রাখে, এমনকি করেকশান হলে ও।
এডিট সহজ: একবার নম্বর সিস্টেমের বাইরে ভিজিবল হলে এটিকে স্থায়ী ধরে নিন। ভাগ না করে পুনঃসংখ্যায়করণ করবেন না। যদি করেকশান দরকার হয়, নতুন ডকুমেন্ট তৈরি করে পুরনোটি রেফার করুন (যেমন ক্রেডিট নোট বা রিপ্লেসমেন্ট টিকেট), কিন্তু ইতিহাস বদলাবেন না।
একটি ব্যবহারিক নিয়ম:
- ড্রাফটে চূড়ান্ত নম্বর নেই (ইন্টারনাল ID বা "DRAFT" ব্যবহার করুন)।
- নম্বর কেবল "Post/Issue"-এ বরাদ্দ করুন, একই ট্রানজ্যাকশনে স্ট্যাটাস চেঞ্জের সাথে।
- ভয়েড ও ক্যানসেল নম্বর রাখে, কিন্তু স্পষ্ট স্ট্যাটাস ও কারণ যোগ করুন।
- প্রিন্ট/ইমেইল করা নম্বর বদলাবেন না।
- ইমপোর্টস অরিজিনাল নম্বর রাখে এবং কাউন্টারকে সর্বোচ্চ আমদানি করা মানের পরে সেট করে।
মাইগ্রেশন ও ইমপোর্ট বিশেষ যত্ন দাবী করে। অন্য সিস্টেম থেকে সরালে বিদ্যমান ইনভয়েস নম্বরগুলো যেমন আছে তেমনই নিয়ে আসুন, তারপর আপনার কাউন্টারকে সর্বোচ্চ ইমপোর্টকৃত মানের পরে শুরু করুন। বিভিন্ন ফর্ম্যাট কনফ্লিক্ট (বছরের ভিন্ন প্রিফিক্স ইত্যাদি) থাকলে ডিসপ্লে নম্বর ঠিক একইভাবে স্টোর করা ভাল এবং একটি আলাদা ইন্টারনাল প্রাইমারি কী রাখুন।
উদাহরণ: একটি হেল্পডেস্ক দ্রুত টিকেট তৈরি করে, কিন্তু অনেক ড্রাফট থাকে। টিকেট নম্বর কেবল তখনই দিন যখন এজেন্ট "Send to customer" ক্লিক করে। এতে পরিত্যক্ত ড্রাফটে নম্বর নষ্ট হয় না এবং দৃশ্যমান সিকুয়েন্স সত্যিকারের কাস্টমার কমিউনিকেশনের সাথে সারিবদ্ধ থাকে। AppMaster-এ একই ধারণা প্রযোজ্য: ড্রাফটগুলো পাবলিক নম্বর ছাড়া থাকুক, পরে "submit" বিজনেস প্রসেস স্টেপে সফলভাবে কমিটের সময় চূড়ান্ত নম্বর জেনারেট করুন।
ডুপ্লিকেট বা হঠাৎ গ্যাপ ঘটানোর সাধারণ ভুলগুলো
প্রায় সব নম্বরিং সমস্যা এক ভাবনা থেকেই আসে: নম্বরকে একটি ডিসপ্লে ভ্যালু হিসেবে বিবেচনা করা বরং একটি শেয়ার করা স্টেট হিসেবে না দেখা। যখন একাধিক ব্যক্তি একই সময়ে সেভ করে, সিস্টেমের প্রয়োজন একটি স্পষ্ট জায়গা যেখানে পরবর্তী নম্বর নির্ধারিত হবে, এবং ব্যর্থতার ক্ষেত্রে কী হবে তার স্পষ্ট নিয়ম।
ক্লাসিক ভুল হল SELECT MAX(number) + 1 অ্যাপ কোডে ব্যবহার করা। একক-ইউজার টেস্টিং-এ এটা ঠিক মনে হয়, কিন্তু দুইটি অনুরোধ একই MAX পড়তে পারে আগে কোনোটি কমিট করে। উভয়ই একই পরবর্তী মান তৈরি করে, আপনি ডুপ্লিকেট পাবেন। এমনকি আপনি যদি "চেক করে পরে রিট্রাই" যোগ করেন, পিক টাইমে আপনি বেশি লোড ও অদ্ভুত স্পাইক পাবেন।
আরেকটি সাধারণ উৎস হলো ক্লায়েন্ট সাইডে নম্বর জেনারেট করা (ব্রাউজার বা মোবাইল) পরে সেভ করার আগে। ক্লায়েন্ট জানে না অন্য ব্যবহারকারীরা কী করছে, এবং এটি নিরাপদভাবে নম্বর রিজার্ভ করতে পারে না যদি সেভ ব্যর্থ হয়। ক্লায়েন্ট-জেনারেটেড নম্বর সাময়িক লেবেল যেমন "Draft 12"-এর জন্য ঠিক আছে, কিন্তু অফিসিয়াল ইনভয়েস বা টিকেট ID-র জন্য নয়।
গ্যাপ নিয়ে হতাশা সেই টিমগুলোকে হয় যারা সিকোয়েন্সকে gapless আশা করে। PostgreSQL-এ sequences ইউনিকনের জন্য ডিজাইন করা—পারফেক্ট কন্টিনিউটি নয়। ট্রানজ্যাকশন রোলব্যাক করলে, প্রিফেচ আইডি করলে, বা ডাটাবেস রিস্টার্ট করলে ভ্যালু স্কিপ হতে পারে। এটা স্বাভাবিক আচরণ। যদি আপনার বাস্তব চাহিদা "কোনো ডুপ্লিকেট নয়" হয়, sequence + unique constraint সাধারণত সঠিক উত্তর। যদি আপনার দাবি সত্যিই "gapless", আলাদা প্যাটার্ন (সাধারণত row locking) দরকার এবং আপনি থ্রুপুটে কিছু ট্রেডঅফ মেনে নিবেন।
লকিংও ব্যर्थ হতে পারে যখন খুব বিস্তৃতভাবে প্রয়োগ করা হয়। সবকিছুর জন্য একটি গ্লোবাল লক দিলে প্রতিটি ক্রিয়েট অ্যাকশন লাইনে পড়ে যাবে, এমনকি আপনি কোম্পানি, লোকেশন বা ডকুমেন্ট টাইপ আলাদা করে পার্টিশন করলে দ্রুত করা যেত। এতে পুরো সিস্টেম ধীর হয়ে যায় এবং ইউজারদের মনে হতে পারে সেভ "এলোমেলোভাবে" আটকে যাচ্ছে।
নিচে পরীক্ষা করার মতো ভুলগুলো:
MAX + 1(বা "find last number") ব্যবহার করা בלי ডাটাবেস-লেভেল unique constraint।- চূড়ান্ত নম্বর ক্লায়েন্টে জেনারেট করে পরে কনফ্লিক্ট "ফিক্স" করার চেষ্টা করা।
- PostgreSQL sequences-কে gapless মনে করা, পরে গ্যাপকে ত্রুটি মনে করা।
- সবকিছুর জন্য একটি শেয়ার্ড কাউন্টার লক করা, পার্টিশন না করা যেখানে তা অর্থবহ।
- মাত্র এক ইউজার দিয়ে টেস্ট করা, যাতে রেস কন্ডিশন লঞ্চ পর্যন্ত দেখা না যায়।
প্রায়োগিক টেস্ট টিপ: 100 থেকে 1,000 রেকর্ড সমান্তরালে তৈরি করার মতো কনকারেন্সি টেস্ট চালান এবং পরে ডুপ্লিকেট ও অপ্রত্যাশিত গ্যাপ চেক করুন। AppMaster-এ বানালে একই নিয়ম: চূড়ান্ত নম্বর সার্ভার-সাইড একক ট্রানজ্যাকশনে অ্যাসাইন করুন, না যে UI-ফ্লোতে।
চালানো থেকে আগে দ্রুত চেকলিস্ট
ইনভয়েস বা টিকেট নম্বরিং রোলআউট করার আগে দ্রুত একটি পাস দিন যেগুলো বাস্তবে লোডে ব্যর্থ হয়। লক্ষ্য সহজ: প্রতিটি রেকর্ড পায় ঠিক একটা বিজনেস নম্বর, এবং আপনার নিয়মগুলো সঠিক থাকে যখন 50 জন একই সঙ্গে "Create" চাপবে।
প্রী-শিপ চেকলিস্ট:
- নিশ্চিত করুন বিজনেস নম্বর ফিল্ডে ডাটাবেসে একটি unique constraint আছে (শুধুমাত্র UI চেক নয়)। এটি হলো আপনার শেষ রক্ষা যদি দুই অনুরোধ সংঘর্ষ করে।
- নিশ্চিত করুন নম্বর অ্যাসাইন করা হচ্ছে সেই একই ডাটাবেস ট্রানজ্যাকশনে যা রেকর্ড সেভ করে। যদি নম্বর অ্যাসাইন ও সেভ আলাদা অনুরোধে থাকলে, শেষমেশ ডুপ্লিকেট দেখা যাবে।
- যদি gapless চাইতে হয়, নম্বর কেবল ফাইনালাইজ করার সময় অর্পণ করুন (উদাহরণ: ইনভয়েস ইস্যু করা হলে, না যে ড্রাফট তৈরি করার সময়)। ড্রাফট, পরিত্যক্ত ফর্ম, ব্যর্থ পেমেন্ট গ্যাপের সাধারণ উৎস।
- বিরল কনফ্লিক্টগুলোর জন্য একটি রিট্রাই স্ট্র্যাটেজি যোগ করুন। row locking বা sequence থাকলেও কখনো কখনো serialization error, deadlock বা unique violation দেখা দিতে পারে। ছোট ব্যাকঅফসহ একটি সহজ রিট্রাই প্রায়ই যথেষ্ট।
- UI, public API এবং bulk imports—এসব এন্ট্রি পয়েন্টে 20 থেকে 100 সমান্তরাল ক্রিয়েট স্ট্রেস টেস্ট করুন। বাস্তবসম্মত মিশ্রণ যেমন বার্স্ট, ধীর নেটওয়ার্ক, ও ডাবল সাবমিট টেস্ট করুন।
আপনার সেটআপ যাচাই করার দ্রুত উপায়: একটি ব্যস্ত হেল্পডেস্ক মুহূর্ত সিমুলেট করুন—দুই এজেন্ট "New ticket" ফর্ম খুলেছে, একজন ওয়েব অ্যাপ থেকে সাবমিট করছে, একই সময়ে ইমেইল ইনবক্স থেকে ইমপোর্ট জব টিকেট ইনসার্ট করছে। রান শেষে চেক করুন সব নম্বর ইউনিক, সঠিক ফর্ম্যাটের, এবং ব্যর্থতা অর্ধেক-সেভড রেকর্ড ছেড়ে যায়নি।
AppMaster-এ কাজ করলে একই নীতি প্রযোজ্য: নম্বর অ্যাসাইনমেন্ট ডাটাবেস ট্রানজ্যাকশনে রাখুন, PostgreSQL কনস্ট্রেইন্টের উপর নির্ভর করুন, এবং UI ও API উভয় এন্ট্রি পয়েন্ট টেস্ট করুন। অনেক দল ম্যানুয়াল টেস্টে সেফ ফিল করে কিন্তু প্রথম দিনই বাস্তব ইউজার বাড়লে চমকে যায়—এটাই এড়াতে হবে।
উদাহরণ: ব্যস্ত হেল্পডেস্ক টিকেট ও পরবর্তী পদক্ষেপ
ভাবুন একটি সাপোর্ট ডেস্ক যেখানে এজেন্টরা সারাদিন টিকেট তৈরি করে, আর একটি ইন্টিগ্রেশন চ্যাট টুল ও ইমেইল থেকেও টিকেট তৈরি করে। সবাই টিকেট নম্বর আশা করে T-2026-000123 ধাঁচে, এবং প্রত্যাশা করে প্রতিটি নম্বর একটিই টিকেট নির্দেশ করবে।
নৈরাজ্য পন্থা: "শেষ টিকেট নম্বর পড়ো", 1 যোগ করো, তারপর সেভ করো। লোডে, দুটো অনুরোধ একই "শেষ নম্বর" পড়তে পারে আগে কোনোটি সেভ করে—উভয়ই একই পরবর্তী নম্বর গণনা করবে এবং ডুপ্লিকেট হবে। যদি আপনি ব্যর্থ হলে রিট্রাই করে সমাধান করার চেষ্টা করেন, আপনি অনিচ্ছায় গ্যাপ তৈরি করতে পারেন।
ডাটাবেস যদি আপনার কোড ভাঙঁইও, তখন ডুপ্লিকেট বন্ধ করতে পারে। ticket_number কলামে unique constraint যোগ করুন। তারপর, যখন দুই অনুরোধ একই নম্বর চেষ্টা করে, একটি ইনসার্ট ব্যর্থ হবে এবং আপনি পরিষ্কারভাবে রিট্রাই করতে পারবেন। এটিই ইনভয়েস নম্বরিং-এর মূল: ডাটাবেসকে ইউনিকনেস নিশ্চিত করতে দিন, UI-কে নয়।
Gapless নম্বরিং workflow পরিবর্তন করে। যদি আপনি গ্যাপ চাই না, আপনি সাধারণত টিকেট তৈরি হওয়ার সময় (ড্রাফট) চূড়ান্ত নম্বর দিতে পারবেন না। পরিবর্তে, টিকেট ডিফল্ট স্ট্যাটাস Draft নিয়ে তৈরি করুন এবং ticket_number খালি রাখুন। নম্বর কেবল টিকেট ফাইনালাইজ করার সময় দেওয়া হবে, যাতে ব্যর্থ সেভ ও পরিত্যক্ত ড্রাফট নম্বর নষ্ট না করে।
সরল টেবিল ডিজাইন:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (যেমন "tickets_2026"), next_number
AppMaster-এ আপনি এটা Data Designer-এ মডেল করতে পারেন এবং Business Process Editor-এ লজিক বানাতে পারেন:
- Create Ticket: status=Draft দিয়ে ticket ইনসার্ট করুন, ticket_number নেই
- Finalize Ticket: একটি ট্রানজ্যাকশন শুরু করুন, কাউন্টার সারি লক করুন, ticket_number সেট করুন, next_number ইনক্রিমেন্ট করুন, কমিট করুন
- Test: একই সময়ে দুটি "Finalize" চালান এবং নিশ্চিত করুন আপনি কখনো ডুপ্লিকেট পাচ্ছেন না
পরবর্তী পদক্ষেপ: আপনার নিয়ম নির্ধারণ করুন (শুধু ইউনিক বনাম সত্যিকারের gapless)। আপনি যদি গ্যাপ মেনে নিতে পারেন, ডাটাবেস sequence + unique constraint সাধারণত যথেষ্ট এবং ফ্লো সহজ রাখে। যদি gapless বাধ্যতামূলক, নম্বরিংকে finalization স্টেপে সরান এবং "draft"-কে ফুল স্টেট হিসেবে বিবেচনা করুন। এরপর মাল্টিপল এজেন্ট একসাথে ক্লিক করলে ও API ইন্টিগ্রেশন বার্স্ট পাঠালে লোড-টেস্ট করুন, যাতে ব্যবহারকারীরা আসার আগেই আচরণ দেখা যায়।


