Systems Hub
// Real-World Backend Engineering

তুমি কি জানো,
বেশিরভাগ Backend
কেন ভেঙে পড়ে?

Raw SQL লেখা, copy-paste করা query, কোনো structure নেই — এই ভুলগুলো ছোট project এ ধরা পড়ে না।
কিন্তু যখন লাখ লাখ user আসে, তখন সব ভেঙে পড়ে।

আজকে দেখবো — Prisma + PostgreSQL দিয়ে কীভাবে এমন একটা system বানানো যায় যেটা reusable, maintainable, এবং scalable — production-ready কোড দিয়ে।

PostgreSQL Prisma ORM Design Patterns Reusable Architecture
🐘
PostgreSQL
02
// The Problem

যে ভুলটা সবাই করে

Junior থেকে mid-level — সবাই এই trap এ পড়ে।

❌ সমস্যা যেভাবে লেখে
// Route handler এ সরাসরি DB call app.get('/courses', async (req, res) => { const page = req.query.page || 1; const limit = req.query.limit || 10; // কোনো validation নেই // কোনো error handling নেই // duplicate code হবে সব জায়গায় const data = await prisma.course.findMany({ skip: (page - 1) * limit, take: Number(limit), // search? filter? sort? // copy করতে হবে আবার... }); res.json(data); });
✅ Production-ready পদ্ধতি
// Clean service layer separation const { skip, take, orderBy, where } = buildQueryOptions( query, searchableFields, sortableFields, filterableFields ); const [data, total] = await Promise.all([ prisma.course.findMany({ where, skip, take, orderBy }), prisma.course.count({ where }), ]); // ✅ Reusable everywhere // ✅ Search + Sort + Filter + Pagination
এই পার্থক্যটা বোঝা দরকার কারণ...
১০০টা endpoint এ আলাদা আলাদা pagination logic = ১০০টা জায়গায় bug। একটা reusable helper = একবার fix, সব ঠিক।
03
// Architecture

Module-Based Architecture

প্রতিটা feature তার নিজের layer এ থাকে — সব কিছু predictable।

src/app/modules/ ├── student/ │ ├── courseStudent.routes.js │ ├── courseStudent.controller.js │ ├── courseStudent.services.js │ ├── courseStudent.constants.js │ └── courseStudent.cache.js ├── admin/ │ ├── class.routes.js │ ├── class.controller.js │ └── class.services.js src/helper/ ├── buildQueryOptions.js ← shared └── activityLog.js ← shared
01
Route Layer
URL mapping, middleware attach, validation
routes.js
02
Controller Layer
Request parse, response send — কোনো business logic নেই
controller.js
03
Service Layer
সব business logic এখানে — Prisma call করে
services.js
04
Constants Layer
searchableFields, filterableFields, sortableFields
constants.js
05
Cache Layer
Redis-backed cache, DB load কমায়
cache.js
04
// The Magic Helper

buildQueryOptions()

একটাই function — pagination, search, sort, filter সব handle করে। লিখতে হয় একবার, ব্যবহার হয় সব জায়গায়।

export const buildQueryOptions = ( query = {}, searchableFields = [], // partial match sortableFields = [], // allowed sorts exactMatchFields = [], // filter=value ) => { const page = parseInt(query?.page) || 1; const limit = Math.min( parseInt(query?.limit) || 10, 1000 ); const sortBy = query?.sortBy || 'createdAt'; const sortOrder = query?.sortOrder || 'desc'; const skip = (page - 1) * limit; const orderBy = sortableFields.includes(sortBy) ? { [sortBy]: sortOrder === 'asc' ? 'asc' : 'desc' } : {}; // search বা exact filter — দুটো একসাথে না const where = buildWhere( query, searchableFields, exactMatchFields ); return { skip, take: limit, orderBy, where }; };
Query Parameters → Prisma Options
?searchTerm=math OR [contains: "math"]
?filter=active OR [equals: "active"]
?page=2&limit=20 skip: 20, take: 20
?sortBy=title&sortOrder=asc orderBy: {title: 'asc'}
Security: limit max 1000 — DDoS protection built-in
Nested Field Support
"course.title"{ course: { title: { contains: ... } } }
Prisma এর nested relation query automatic।
05
// Constants Pattern

Constants দিয়ে Behavior Control

কোন field এ search হবে, কোনটায় sort হবে — এটা code এ hardcode না করে, constants file এ declare করো।

constants.js
export const searchableFields = [ 'course.title', 'course.productName', 'student.fullName', ← nested! ]; export const sortableFields = [ 'createdAt', 'updatedAt', 'enrolledAt', ]; export const filterableFields = [ 'status', 'paymentStatus', ]; export const selectFields = { id: true, studentId: true, course: { select: { title: true, courseImage: true } } };
কেন এটা ভালো?
Service file খুলতে হয় না। শুধু constants.js বদলালেই পুরো module এর behavior বদলে যায়। নতুন developer ও instantly বুঝতে পারে।
🔒
SQL Injection Protection
sortableFields.includes(sortBy) check করে — user যা পাঠাক, শুধু allow-listed field এ sort হবে।
Select Optimization
Prisma এর select দিয়ে শুধু দরকারি column fetch — unnecessary data transfer নেই।
🔄
Module Independence
প্রতিটা module এর আলাদা constants — student module এর search field admin module কে affect করে না।
06
// Validation Pattern

Zod + Middleware = Clean Validation

Database এ ভুল data ঢোকার আগেই block করো — Zod schema + reusable middleware দিয়ে।

// validation.middleware.js const validationRequest = (schema) => { return async (req, res, next) => { try { const parsed = await schema.parseAsync({ body: req.body, query: req.query, params: req.params, }); req.body = parsed.body; next(); } catch (err) { next(err); // Zod error → global handler } }; }; // routes.js এ use: router.post('/enroll', validationRequest(enrollmentSchema), controller.enroll );
// Zod schema const enrollmentSchema = z.object({ body: z.object({ courseId: z.string().uuid(), paymentMethod: z.enum([ 'BKASH', 'NAGAD', 'BANK' ]), }) });
Request Flow
Client Request
Zod Validate
❌ Invalid
Service Layer
Prisma → DB
Type check, format check — controller এ একটা line ও লাগে না
Error message automatic — Zod বলে দেয় ঠিক কোন field ভুল
Schema reuse করা যায় — create ও update এ same schema extend
TypeScript-like type safety — JavaScript এও
07
// Error Handling Pattern

Centralized Error Handling

প্রতিটা route এ try/catch লিখলে code mess হয়। একটা centralized system দিয়ে সব errors handle করো।

// AppErrors.js — Custom Error Class class AppErrors extends Error { constructor(statusCode, message) { super(message); this.statusCode = statusCode; this.isOperational = true; } } // Service এ throw করো: if (!user) throw new AppErrors( StatusCodes.NOT_FOUND, 'User not found' ); if (!hasAccess) throw new AppErrors( StatusCodes.FORBIDDEN, 'Course access denied' ); // Global error handler (app.js): app.use((err, req, res, next) => { if (err.isOperational) { res.status(err.statusCode).json({ success: false, message: err.message }); } else { // Unexpected error — log & 500 console.error(err); res.status(500).json({ ... }); } });
0
Route এ try/catch দরকার
1
Global handler সব handle করে
isOperational Flag
AppErrors দিয়ে throw হলে isOperational = true — এটা expected error (user না পাওয়া, permission নেই)। Unexpected crash হলে false — আলাদাভাবে log করো।
NOT_FOUND (404) user/course/data নেই
FORBIDDEN (403) access নেই
UNAUTHORIZED (401) token নেই/expired
CONFLICT (409) duplicate data
08
// Cache Layer

Redis Cache on Top of Prisma

লাখ user এর জন্য প্রতিটা request এ DB hit করলে system মরবে। Redis cache দিয়ে DB load ৯০%+ কমাও।

// getOrLoadStrictCache pattern const getCachedAuthUser = async (userId) => { const cacheKey = `auth:user:${userId}`; return await getOrLoadStrictCache( cacheKey, async () => { // শুধু cache miss হলে DB call return await prisma.user.findUnique({ where: { id: userId }, select: { id: true, role: true, status: true } }); }, { ttl: 120 } // 2 minutes TTL ); }; // Enrollment change হলে cache invalidate: await invalidateCourseStudentAccess( studentId, courseId );
Cache Keys Architecture
🔑 auth:user:{userId} 2min TTL
🔑 auth:session:{userId} 2min TTL
🔑 course:access:{studentId}:{courseId} auto clear
🔑 course:context:{entityId} 2min TTL
Cache Invalidation Strategy
Data বদলালে cache clear করতে হয়। Enrollment হলে → invalidateCourseStudentAccess()। Many students এ → invalidateCourseStudentAccessMany()
09
// End-to-End Flow

একটা API Request এর পুরো journey

GET /my-courses request পাঠালে কী হয় — step by step।

01
Route → validationRequest(schema) → authorize(['student'])
Zod দিয়ে query validate, তারপর JWT verify + role check
~2ms
02
Redis Cache Check — getCachedAuthUser(userId)
User data cache এ আছে? → directly use। নেই? → Prisma call
~1ms hit
03
Controller → service.getMyCourses(query, payload)
Request parsed, payload (userId, role) extracted থেকে service call
~0ms
04
buildQueryOptions(query, fields…) → Prisma options
search, sort, filter, pagination — সব resolve হয়ে যায় এখানে
~0ms
05
Promise.all([findMany, count]) → PostgreSQL
Data + total count — দুটো query parallel এ চলে, time অর্ধেক
~15ms
06
Response: { data, meta: { page, limit, total } }
Consistent response format — সব endpoint এ same structure
✓ Done
10
// Summary

এই Patterns গুলো দিয়ে কী পেলাম?

একটা production system এ এই design decisions এর impact।

♻️
Reusability
buildQueryOptions একবার লেখা, ৫০+ endpoint এ use। এক জায়গায় fix = সব জায়গায় fix।
🧩
Maintainability
Module-based structure — নতুন developer আসলে instantly বুঝতে পারে কোথায় কী আছে।
🚀
Scalability
Redis cache + Prisma optimize query — লাখ user এও DB overwhelm হয় না।
🔒
Security
Zod validation, allowlist sort fields, AppErrors — সব layer এ protection।
🐛
Debuggability
Centralized error handling + activity log — production issue instantly track করা যায়।
Performance
Promise.all parallel queries, select optimization, limit max 1000 — সব কিছু intentional।
মূল কথা
Good architecture মানে বেশি code লেখা না — মানে কম code দিয়ে বেশি করা। এই patterns গুলো practice করো, তোমার project ও production-ready হবে।