Warming up the neural circuits...
By the end of this chapter you will:
Rules like "always validate at the boundary" or "never mutate shared " seem abstract until the day you inherit a codebase that ignored them. This chapter is the philosophy behind all the technical decisions in the previous 16 chapters. Read it once, then refer back when you're making a judgment call.
SOLID is a set of five principles for writing code that's easy to change, test, and understand.
Every class or function does one thing and does it well.
// ❌ OrdersService does too much
class OrdersService {
async create(dto) { /* order logic */ }
async sendEmail(orderId) { /* email logic */ } // belongs in EmailService
async syncWithVendor(orderId) { /* vendor logic */ } // belongs in VendorService
}
// ✅ Each service owns one domain
class OrdersService { /* only order business rules */ }
class EmailService { /* only email sending */ }
class VendorSyncService { /* only vendor API calls */ }Open for extension, closed for modification. Add new behavior without changing existing code.
// ❌ Adding a new payment method requires modifying this function
function processPayment(method: string) {
if (method === 'card') { /* ... */ }
if (method === 'upi') { /* ...
A subclass should be usable wherever the parent class is expected. In NestJS: any class implementing an interface should be swappable with any other. This is why DI with interfaces is powerful — you can swap a real S3Service for a MockS3Service in tests without changing the code that uses it.
Don't force a class to implement methods it doesn't use. Keep interfaces small and focused.
// ❌ Fat interface — every implementer must implement everything
interface UserService {
create(); update(); delete(); sendEmail(); generateReport();
}
// ✅ Small, focused interfaces
interface UserRepository { create(); update(); delete(); }
interface UserNotifier { sendEmail(); }
Depend on abstractions, not concrete implementations. This is literally what NestJS's DI system does.
// ❌ Service creates its own dependency — can't test without a real DB
class OrdersService {
private orderModel = new Order(); // hard-coded
}
// ✅ Dependency is injected — test can inject a mock
class OrdersService {
constructor(
@Inject('ORDER_REPOSITORY') private orderModel:
DRY means "every piece of knowledge should have a single, unambiguous representation in the system."
It does NOT mean "never write similar-looking code." These are different things.
// Two functions that look similar but represent DIFFERENT knowledge
// Do NOT merge them into one "generic" function
async getOrderById(id: string, userId: string) {
return this.orderModel.findOne({ where
DRY is about knowledge, not code. Duplicate code that represents the same rule → fix it. Similar-looking code that represents different rules → leave it alone.
Don't build features for imaginary future requirements. Build what's needed today, cleanly, so it's easy to extend later.
// ❌ YAGNI violation — building "just in case" features
interface CreateOrderDto {
restaurantId: string;
items: OrderItemDto[];
future_batch_mode?: boolean; // no one asked for this
legacy_system_ref?: string; // "might need someday"
multi_currency_mode
When something is wrong, find out immediately and loudly — not 3 hours later when a customer calls.
JWT_SECRET is missing// ❌ Silent failure — the bug hides until a user sees wrong data
const amount = parseFloat(dto.amount) || 0; // if amount is 'abc', you process 0
// ✅ Loud failure — you find out immediately
const amount = parseFloat(dto.amount);
if (isNaNStructure your code so that the easy path is the correct path.
transaction parameter (can't accidentally forget it)ValidationPipe global (can't accidentally skip )@UseGuards(JwtGuard) required per-routeIf forgetting a rule causes an immediate error — a compile error, a test failure, an app crash on startup — you get the feedback before it reaches production. If forgetting a rule is silent, it will reach production.
This is a fintech app with real money. This is not the place to use the latest experimental framework.
Node.js + NestJS + PostgreSQL + Redis: not the newest, not the fastest — but battle-tested and with solutions for every problem. PostgreSQL is 30 years old. That's not old, that's proven.
"Let me use this new serverless DB that just came out of beta." In a user-facing food delivery app, boring beats clever every time. Don’t be the person who introduced an experimental dependency that took down the whole platform.
When calling external services (vendor APIs, mail servers, payment gateways), assume they will fail.
| Pattern | What it does | Use when |
|---|---|---|
| Timeout | Give up if call takes too long | Every outbound HTTP call |
| Retry with backoff | Try again, wait longer each time | Transient failures (vendor returns 503) |
| Circuit Breaker | Stop calling a failing service temporarily | Vendor is reliably down |
| Bulkhead | Isolate failures (one vendor's problems don't affect other vendors) | Multiple vendor integrations |
| Idempotency | Same operation → same result (safe to retry) | jobs, payment charges |
| Dead-letter queue | Capture permanently failing jobs for manual review | Queue workers |
configService.get().trace_id in log messages.@UseGuards(JwtGuard) on every endpoint. No exceptions.successResponse() / errorResponse(). No raw objects.console.log in production code. Use the Winston logger.Seq Scan on a large table — add an index.latest in production.src/modules/
├── orders/
│ ├── orders.controller.ts
│ ├── orders.controller.spec.ts
│ ├── orders.service.ts
│ ├── orders.service.spec.ts
│ ├── orders.repository.ts
│ ├── orders.module.ts
│ └── dto/
│ ├── create-order.dto.ts
│ ├── update-order.dto.ts
│ └── order.response.ts
├── restaurants/
├── users/
└── payments/
src/database/
└── models/│ └── my-feature.model.ts └── providers/ │ └── model-repository.provider.ts └── models/index.ts
-script/ └── 2026-05-01_add_my_feature_table.sql
**Registration checklist:**
- [ ] Model added to `src/database/models/index.ts`
- [ ] Repository provider added to `src/database/providers/model-repository.provider.ts`
- [ ] Controller added to `AgentPortalModule` controllers array
- [ ] Service added to `AgentPortalModule` providers array
- [ ] SQL migration created in `sql-script/`
- [ ] Swagger `@ApiTags('my-feature')` on controller
---
## Appendix B — Further reading
| Resource | Why read it |
|----------|------------|
| NestJS docs | The authoritative reference. Read Fundamentals and Techniques. |
| Sequelize docs | Model associations and querying are the parts most people get wrong. |
| PostgreSQL docs — Indexes | Understanding B-tree indexes will save you from many slow queries. |
| OWASP Top 10 | The 10 most common security vulnerabilities. Know all of them. |
| The 12-factor app | 10-minute read. Will shape how you think about deployment for years. |
| Martin Fowler — P of EAA | Patterns of Enterprise Application Architecture — where Repository, Service Layer, and many patterns come from. |
| Designing Data-Intensive Applications | The definitive book on databases, distributed systems, and scalability. |
---
## You've reached the end of the core chapters
You now know:
- What a backend is and why it's structured the way it is
- Every library in this project and what it does
- How to design a database, write migrations, build models, controllers, services
- How to authenticate users, validate input, manage config and secrets
- How to observe and debug a running system
- How to test, optimize performance, use async queues, and deploy safely
If the business rules for orders and transactions diverge (they will), you'll be glad you kept them separate.