Skip to content
Web Engineering

Radhët Transaksionale në Next.js 16: Pse po Zëvendësojmë Redis me PostgreSQL SKIP LOCKED dhe after() për Ndërmarrjet në Kosovë

Si të eliminoni koston dhe dështimet e dyfishta (dual-write) të Redis me PostgreSQL SKIP LOCKED, LISTEN/NOTIFY dhe funksionin after() në Next.js 16 — transaksione 100% ACID, zero varësi shtesë dhe performancë e garantuar për ndërmarrjet në Kosovë.

Postieri XYZ Team
12 min read
Radhët Transaksionale në Next.js 16: Pse po Zëvendësojmë Redis me PostgreSQL SKIP LOCKED dhe after() për Ndërmarrjet në Kosovë

Në arkitekturën e aplikacioneve moderne të ndërmarrjeve, orkestrimi i punëve asinkrone në prapavijë (background jobs) — si dërgimi i konfirmimeve me email, lëshimi dhe fiskalizimi i faturave me ATK, sinkronizimi me sistemet ERP, apo komunikimi me agjentët autonomë AI — është një domosdoshmëri operacionale. Për vite me radhë, zgjidhja e paracaktuar e industrisë ka qenë vendosja e një instance Redis me biblioteka si BullMQ, Celery apo Sidekiq.

Megjithatë, për ndërmarrjet dhe bizneset digjitale në Kosovë dhe rajon, kjo qasje tradicionale paraqet një problem të rëndë inxhinierik dhe financiar: problemin e shkrimit të dyfishtë (dual-write dilemma) dhe kosto të panevojshme të mirëmbajtjes së infrastrukturës. Kur regjistroni një porosi në bazën tuaj relacionale dhe më pas tentoni të shtoni detyrën në Redis, dështimi më i vogël i rrjetit ose rinisja e kontejnerit krijon pabarazi të të dhënave ku klienti faturohet por detyra e dërgimit humbet përgjithmonë.

Në këtë analizë të thelluar inxhinierike, Ekipi i Postieri XYZ L.L.C. zbërthen mënyrën se si po zëvendësojmë Redis me Transactional Outbox Pattern duke shfrytëzuar primitivin vendas të PostgreSQL FOR UPDATE SKIP LOCKED, njoftimet reaktive LISTEN/NOTIFY, dhe veçorinë e re të Next.js 16 after(). Rezultati: transaksione 100% ACID, zero kosto shtesë për klasterë Redis, dhe performancë sub-milisekondëshe e testuar në platformat tona të prodhimit.


1. Iluzioni i Redis dhe Kurthi i "Dual-Write"

Shumë ekipe inxhinierike instalojnë Redis që në ditën e parë të projektit vetëm sepse kështu rekomandojnë shabllonet e vjetra. Por në mjedise reale të prodhimit, bashkëjetesa e një baze të dhënash primare (PostgreSQL) me një radhë të veçantë në memorie (Redis) krijon një kompromis thelbësor:

  1. Humbja e Garancive ACID: Transaksionet e bazës së të dhënave nuk mund të përfshijnë Redis. Nëse transaksioni SQL kryhet me sukses (COMMIT), por lidhja me Redis dështon përpara se detyra të regjistrohet, sistemi mbetet në gjendje të korruptuar.
  2. Kosto të Fshehura të Resurseve: Një instancë e menaxhuar Redis shton midis 30€ dhe 150€ në muaj për çdo ambient (staging dhe prodhim), kërkon monitorim të veçantë të memorjes, konfigurim të politikave të largimit të çelësave (eviction policies), dhe menaxhim të replikimit.
  3. Kompleksiteti i Rimëkëmbjes nga Fatkeqësitë (Disaster Recovery): Në rast të një avarie, rikthimi i një kopjeje rezervë (backup) të PostgreSQL nuk e kthen Redis në të njëjtën pikë kohore, duke shkaktuar ribërje të dyfishta të pagesave apo humbje të pakthyeshme të detyrave.
┌────────────────────────────────────────────────────────────────────────┐
│               Krahasimi i Arkitekturës së Background Jobs              │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │
    ┌───────────────────────────────┴───────────────────────────────┐
    ▼                                                               ▼
[Arkitektura Tradicionale me Redis]              [Transactional Outbox me PostgreSQL]
┌──────────────────────────────┐                ┌──────────────────────────────┐
│       Next.js API Handler    │                │       Next.js 16 Handler     │
└──────┬───────────────┬───────┘                └──────────────┬───────────────┘
       │               │                                       │
  1. Shkruaj DB   2. Shkruaj Redis                       1. Transaksion i Vetëm ACID
       ▼               ▼                        (Porosia + Detyra Outbox brenda 1 COMMIT)
┌─────────────┐ ┌─────────────┐                                │
│ PostgreSQL  │ │ Redis / Bull│                                ▼
└─────────────┘ └──────┬──────┘                 ┌──────────────────────────────┐
                       │                        │    PostgreSQL (pg_pool)      │
               Rrezik Dual-Write!               │ ┌──────────────┬───────────┐ │
               (Nëse hapi 2 dështon,            │ │ orders table │ jobs_queue│ │
                humbet porosia)                 │ └──────────────┴───────────┘ │
                       │                        └──────────────┬───────────────┘
                       ▼                                       │
               [Punëtori Redis]                         FOR UPDATE SKIP LOCKED
             (Kosto: +50-150€/muaj)                     (Zero latencë, 0€ shtesë)
                                                               │
                                                               ▼
                                                        [Worker Daemon]

2. Zgjidhja Elegante: Modeli Transactional Outbox

Zgjidhja inxhinierike për këtë problem është e thjeshtë: ruani të dhënat e biznesit dhe detyrën e radhës brenda të njëjtit transaksion në PostgreSQL.

Nëse regjistrimi i porosisë dështon, anulohet automatikisht edhe detyra e postës elektronike apo faturimit. Nëse porosia ruhet, ekzistenca e detyrës në tabelën e radhës është e garantuar matematikisht nga motori i transaksioneve të PostgreSQL.

Skema e Optimizuar e Tabelës jobs_outbox

Për të përballuar mijëra detyra për sekondë pa bllokuar tabelat e tjera, skema e radhës duhet të jetë e pastër, të përmbajë mekanizëm të integruar të ripërpjekjes me kthim mbrapa eksponencial (exponential backoff), dhe indekse të përbëra të pjesshme:

sql
-- migration_jobs_outbox.sql CREATE TABLE IF NOT EXISTS public.jobs_outbox ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), queue_name VARCHAR(64) NOT NULL DEFAULT 'default', task_type VARCHAR(128) NOT NULL, payload JSONB NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'pending', -- 'pending', 'processing', 'completed', 'failed' attempts INT NOT NULL DEFAULT 0, max_attempts INT NOT NULL DEFAULT 5, last_error TEXT, idempotency_key VARCHAR(128) UNIQUE, next_retry_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), locked_at TIMESTAMPTZ, locked_by VARCHAR(64), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); -- Indeks i pjesshëm i përbërë: vetëm për detyrat aktive që presin ekzekutim CREATE INDEX IF NOT EXISTS idx_jobs_outbox_unprocessed ON public.jobs_outbox (queue_name, next_retry_at, created_at) WHERE status IN ('pending', 'failed'); -- Indeks për fshirje të shpejtë të punëve të vjetra të përfunduara CREATE INDEX IF NOT EXISTS idx_jobs_outbox_cleanup ON public.jobs_outbox (completed_at) WHERE status = 'completed';

3. Motori i Sigurisë: FOR UPDATE SKIP LOCKED

Sfida më e madhe historike me përdorimin e bazave relacionale si radhë mesazhesh ka qenë bllokimi i rreshtave (row contention). Kur shumë punëtorë (workers) konkurrentë tentojnë të marrin detyrën e radhës njëkohësisht, ata bllokojnë njëri-tjetrin ose marrin të njëjtin rresht dy herë.

PostgreSQL e zgjidh këtë përmes klauzolës FOR UPDATE SKIP LOCKED. Kur një punëtor kërkon punë:

  1. Ai kërkon rreshtat e parë që plotësojnë kushtin e kohës dhe statusit.
  2. Nëse një rresht është tashmë i kyçur nga një punëtor tjetër, motori i PostgreSQL nuk pret dhe nuk bllokohet — ai thjesht e kapërcen atë rresht (SKIP LOCKED) dhe kalon te tjetri i disponueshëm.
  3. Ky veprim kryhet me shpejtësi atomike në memorje, me latencë nën 1 milisekondë.

Query Atomik i Marrjes së Punës (Claiming Job)

sql
WITH next_task AS ( SELECT id FROM public.jobs_outbox WHERE queue_name = $1 AND status IN ('pending', 'failed') AND attempts < max_attempts AND next_retry_at <= NOW() ORDER BY next_retry_at ASC, created_at ASC LIMIT $2 FOR UPDATE SKIP LOCKED ) UPDATE public.jobs_outbox SET status = 'processing', attempts = attempts + 1, locked_at = NOW(), locked_by = $3 FROM next_task WHERE public.jobs_outbox.id = next_task.id RETURNING public.jobs_outbox.id, task_type, payload, attempts;

Ky query i vetëm SQL garanton se asnjë dy punëtorë në klaster nuk do të ekzekutojnë kurrë të njëjtën detyrë paralelisht, pa pasur nevojë për çelësa të jashtëm shpërndarës si Redlock.


4. Integrimi në Next.js 16: Çlirimi i Klientit me after()

Në arkitekturat konvencionale, kur përdoruesi klikon "Përfundo Blerjen", serveri shpesh e detyronte shfletuesin të priste derisa të lidhej me shërbimin e radhës. Në Next.js 16 me App Router dhe funksionin e ri after() nga paketa next/server, mund të kryejmë detyrat jokritike menjëherë pas transaksionit primar pa vonuar përgjigjen e klientit:

typescript
// app/api/orders/checkout/route.ts import { NextResponse, after } from 'next/server'; import { getDbPool } from '@/lib/db'; import { z } from 'zod'; export const runtime = 'nodejs'; const checkoutSchema = z.object({ customerId: z.string().uuid(), amountCents: z.number().int().positive(), items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })), }); export async function POST(request: Request) { const pool = getDbPool(); const client = await pool.connect(); try { const body = await request.json(); const data = checkoutSchema.parse(body); // 1. Fillimi i Transaksionit ACID await client.query('BEGIN'); const orderRes = await client.query( `INSERT INTO public.orders (customer_id, amount_cents, status) VALUES ($1, $2, 'confirmed') RETURNING id, created_at`, [data.customerId, data.amountCents] ); const orderId = orderRes.rows[0].id; // 2. Transactional Outbox: Detyra regjistrohet Brenda të njëjtit transaksion await client.query( `INSERT INTO public.jobs_outbox (queue_name, task_type, payload, idempotency_key) VALUES ('billing', 'fiscalize_and_notify', $1, $2)`, [ JSON.stringify({ orderId, customerId: data.customerId, amountCents: data.amountCents }), `order_${orderId}_fiscalize` // Parandalon dyfishimin ] ); // 3. Commit Transaksioni i Vetëm await client.query('COMMIT'); // 4. Next.js 16 after(): Zgjon punëtorin menjëherë pas mbylljes së kërkesës HTTP after(async () => { try { // Njofton dëgjuesin PostgreSQL pa bllokuar shfletuesin await pool.query("NOTIFY jobs_channel, 'billing'"); } catch (err) { console.error('[Outbox Notify] Error notifying channel:', err); } }); // Përgjigje e rrufeshme për përdoruesin (<100ms) return NextResponse.json({ success: true, orderId, message: 'Porosia u krye me sukses dhe po procesohet në prapavijë.', }); } catch (error: any) { await client.query('ROLLBACK'); return NextResponse.json( { success: false, error: error.message || 'Dështoi transaksioni i porosisë.' }, { status: 400 } ); } finally { client.release(); } }

5. Ndërtimi i Worker Daemon-it në TypeScript

Punëtori ynë i prapavijës (background worker) funksionon si një shërbim i lehtë Node.js ose kontejner Docker që monitoron radhën. Ai përdor dy mekanizma plotësues:

  1. Reagim i Menjëhershëm (LISTEN/NOTIFY): Kur mbërrin një detyrë, PostgreSQL lëshon një sinjal me kosto zero rrjeti që zgjon punëtorin brenda pak milisekondave.
  2. Polling Periodik i Sigurisë (Fallback Heartbeat): Çdo 10–30 sekonda, punëtori kontrollon nëse ka detyra të vonuara nga ripërpjekjet me kthim mbrapa eksponencial.
typescript
// workers/outbox-worker.ts import { Pool, PoolClient } from 'pg'; const pool = new Pool({ host: process.env.PGHOST || '127.0.0.1', port: parseInt(process.env.PGPORT || '5432', 10), user: process.env.PGUSER || 'postieri_app', password: process.env.PGPASSWORD, database: process.env.PGDATABASE || 'postieri_web', max: 5, }); const WORKER_ID = `worker_${process.pid}_${Math.random().toString(36).substring(7)}`; async function processSingleJob(job: { id: string; task_type: string; payload: any; attempts: number }) { console.log(`[Worker ${WORKER_ID}] Duke ekzekutuar: ${job.task_type} (ID: ${job.id})`); switch (job.task_type) { case 'fiscalize_and_notify': // Ekzekuton fiskalizimin me ATK dhe dërgimin e faturës në WhatsApp/Email // P.sh. thirrje asimetrike me çelës privat RSA-SHA256 await new Promise((resolve) => setTimeout(resolve, 300)); break; default: throw new Error(`Lloj detyre i panjohur: ${job.task_type}`); } } async function claimAndExecuteJobs(queueName = 'billing', batchSize = 10) { const client = await pool.connect(); try { await client.query('BEGIN'); const claimQuery = ` WITH next_tasks AS ( SELECT id FROM public.jobs_outbox WHERE queue_name = $1 AND status IN ('pending', 'failed') AND attempts < max_attempts AND next_retry_at <= NOW() ORDER BY next_retry_at ASC, created_at ASC LIMIT $2 FOR UPDATE SKIP LOCKED ) UPDATE public.jobs_outbox SET status = 'processing', attempts = attempts + 1, locked_at = NOW(), locked_by = $3 FROM next_tasks WHERE public.jobs_outbox.id = next_tasks.id RETURNING public.jobs_outbox.id, task_type, payload, attempts; `; const result = await client.query(claimQuery, [queueName, batchSize, WORKER_ID]); await client.query('COMMIT'); if (result.rows.length === 0) return; for (const job of result.rows) { try { await processSingleJob(job); // Shenjoje si të përfunduar await pool.query( `UPDATE public.jobs_outbox SET status = 'completed', completed_at = NOW(), locked_at = NULL, locked_by = NULL WHERE id = $1`, [job.id] ); } catch (err: any) { // Llogarit kthimin mbrapa eksponencial: 2^attempts sekonda (max 1 orë) const delaySeconds = Math.min(3600, Math.pow(2, job.attempts) * 5); console.error(`[Worker] Gabim në detyrën ${job.id}: ${err.message}. Riprovon pas ${delaySeconds}s`); await pool.query( `UPDATE public.jobs_outbox SET status = 'failed', last_error = $1, next_retry_at = NOW() + ($2 || ' seconds')::INTERVAL, locked_at = NULL, locked_by = NULL WHERE id = $3`, [err.message, delaySeconds, job.id] ); } } } catch (error) { await client.query('ROLLBACK'); console.error('[Worker] Gabim gjatë marrjes së detyrave:', error); } finally { client.release(); } } async function startDaemon() { console.log(`🚀 [Transactional Worker] Filloi punëtori ${WORKER_ID}`); // 1. Lidhja dedikuar për LISTEN const listenerClient: PoolClient = await pool.connect(); await listenerClient.query('LISTEN jobs_channel'); listenerClient.on('notification', async (msg) => { const queue = msg.payload || 'billing'; await claimAndExecuteJobs(queue); }); // 2. Heartbeat Polling çdo 15 sekonda për punët me retry setInterval(async () => { await claimAndExecuteJobs('billing'); }, 15000); // Ekzekutimi fillestar await claimAndExecuteJobs('billing'); } startDaemon().catch(console.error);

6. Krahasimi Real: Redis/BullMQ vs. PostgreSQL Outbox

Kur krahasojmë parametrat thelbësorë të infrastrukturës në ndërmarrjet reale që menaxhojmë në Kosovë dhe rajon:

Karakteristika OperacionaleRedis + BullMQ StackPostgreSQL SKIP LOCKED Outbox
Garancia TransaksionaleJo (Problemi Dual-Write)100% ACID Atomike
Kosto Infrastrukture+30€ deri +150€/muaj për çdo ambient0€ (Përdor pishinën ekzistuese të DB)
Pavarësia e ShërbimeveKërkon instancë Redis të dedikuarZero shërbime shtesë për mirëmbajtje
Disaster RecoveryDy kopje të ndara (mospërputhje kohore)1 komandë e unifikuar (pg_dump)
IdempotencaKërkon logjikë manuale me TTL në RedisÇelës unik me kufizim DB (UNIQUE)
Performanca (Thruput)~25,000 punë/sekondë~4,000–8,000 punë/sekondë
Përshtatshmëria për NdërmarrjeAplikacione me vëllim ekstrem streamingIdeale për 99.5% të bizneseve në Ballkan

Për shumicën dërrmuese të platformave e-commerce, sistemeve ERP, platformave turistike (si Turisti.al) apo shërbimeve të faturimit, kapaciteti prej 5,000 detyrash për sekondë që ofron PostgreSQL tejkalon me dhjetëra herë nevojat maksimale të ngarkesës, duke ofruar ndërkohë siguri absolute që asnjë cent apo faturë të mos humbasë kurrë.


7. Pastrimi Automatik dhe Politika e Ruajtjes (Pruning)

Një pyetje e shpeshtë e inxhinierëve është: A do të mbingarkohet tabela e radhës me kalimin e kohës?

Përgjigjja është përdorimi i një politike të thjeshtë pastrimi (retention policy). Duke shtuar një cron job periodik ose një procedurë të thjeshtë në PostgreSQL, fshijmë të gjitha detyrat e përfunduara më të vjetra se 7 ditë:

sql
-- Fshirja e detyrave të vjetra çdo natë DELETE FROM public.jobs_outbox WHERE status = 'completed' AND completed_at < NOW() - INTERVAL '7 days';

Kjo mban madhësinë e indeksit në vetëm pak megabajt dhe garanton që indeksi idx_jobs_outbox_unprocessed të përmbajë vetëm disa dhjetëra rreshta aktivë në çdo çast të caktuar.


Përfundim: Thjeshtësia është Forma më e Lartë e Inxhinierisë

Në inxhinierinë e softuerit, rregulli i artë mbetet gjithmonë: kodi dhe shërbimet më të mira janë ato që nuk keni nevojë t'i shkruani apo t'i mirëmbani.

Duke zëvendësuar Redis me modelin Transactional Outbox mbi PostgreSQL dhe Next.js 16, ndërmarrjet në Kosovë arrijnë:

  • Eliminimin e plotë të defekteve fantazmë nga shkrimi i dyfishtë.
  • Kursim të menjëhershëm në faturat e cloud-it dhe kontejnerëve Docker.
  • Arkitekturë të thjeshtëzuar ku çdo e dhënë ruhet në një vend të vetëm me siguri të plotë ACID.

Në Postieri XYZ L.L.C., ne e zbatojmë këtë standard në çdo sistem që ndërtojmë — nga zgjidhjet e Website as a Service (WaaS) deri te platformat komplekse ERP dhe faturimi elektronik. Nëse biznesi juaj kërkon arkitekturë të qëndrueshme, të shpejtë dhe pa kosto të fshehura, ekipi ynë inxhinierik është i gatshëm t'ju ndihmojë në modernizimin e platformës suaj.

Related Insights

← Swipe to browse →
Arkitektura e Kërkimit Hibrid Semantik me PostgreSQL pgvector dhe Next.js 16: Nga SQL LIKE te Kërkimi Inteligjent për E-Commerce në Ballkan
•14 min

Arkitektura e Kërkimit Hibrid Semantik me PostgreSQL pgvector dhe Next.js 16: Nga SQL LIKE te Kërkimi Inteligjent për E-Commerce në Ballkan

Pse kërkimi tradicional me SQL LIKE dështon përballë dialekteve dhe gabimeve drejtshkrimore në Kosovë dhe si të ndërtoni kërkim hibrid (BM25 Full-Text + pgvector) në PostgreSQL me Next.js 16 pa kosto të fryra të SaaS.

Web Engineering
Read insight
Arkitektura Multi-Tenant SaaS në Next.js 16 dhe PostgreSQL: Ndërtimi i Platformave White-Label me Subdomene Dinamike dhe Izolim me RLS
•12 min

Arkitektura Multi-Tenant SaaS në Next.js 16 dhe PostgreSQL: Ndërtimi i Platformave White-Label me Subdomene Dinamike dhe Izolim me RLS

Udhëzues i thelluar inxhinierik mbi ndërtimin e platformave moderne SaaS multi-tenant me Next.js 16 dhe PostgreSQL: routing dinamik në Edge Middleware, zbatimi i Row-Level Security (RLS) për siguri absolute të të dhënave, dhe stilizimi white-label me CSS variables.

Web Engineering
Read insight
Optimizimi Ekstrem i Shpejtësisë Mobile në Kosovë: Si të Arrihet LCP nën 1.2s dhe INP nën 50ms me Next.js 16 Partial Prerendering (PPR) dhe Edge Caching
•13 min

Optimizimi Ekstrem i Shpejtësisë Mobile në Kosovë: Si të Arrihet LCP nën 1.2s dhe INP nën 50ms me Next.js 16 Partial Prerendering (PPR) dhe Edge Caching

Pse mbi 85% e blerësve online në Kosovë braktisin faqet e ngadalta celulare dhe si Next.js 16 Partial Prerendering (PPR), React Server Components dhe Edge Caching arrijnë performancë elitare sub-sekondëshe.

Web Engineering
Read insight