Overview
Custom software built for a specific business operation has a unique challenge: it must be flexible enough to evolve with the business, but structured enough to remain maintainable as it grows. The architectural decisions made in the first 6 months determine whether the system is still running smoothly at 5 years - or has been rewritten twice.
This guide covers the architectural patterns IdeaSprout uses when building custom solutions for Indian SMBs.
1. Domain-Driven Design: Start With the Business
Why Architecture Starts With the Domain
The most common cause of custom software failure is not technical - it's a mismatch between the software model and the business model. When developers model the database around technical convenience rather than business concepts, every new business requirement requires a painful data migration.
Bounded Contexts
A large business operation should be divided into bounded contexts - areas of the business with their own language, rules, and data model. Each bounded context becomes a separate module (or service) with a well-defined interface.
Example: A distribution company
Order Management Context:
- Order, OrderLine, Customer, Pricing
Warehouse Context:
- PickList, StockLocation, Inventory, Batch
Delivery Context:
- DeliveryRun, Vehicle, Driver, ProofOfDelivery
Finance Context:
- Invoice, Payment, CreditNote, LedgerThe same concept (e.g., "Customer") may exist in multiple contexts but with different attributes and rules. In Order Management, a Customer has credit limits and pricing tiers. In Delivery, a Customer has a delivery address and time window. These are different models - don't force them into one.
Ubiquitous Language
Every bounded context should have a ubiquitous language - a shared vocabulary used by both developers and business users. When a business user says "pick list", the code should have a PickList class, not a WarehouseTaskGroup.
2. API-First Architecture
Why API-First
Building the API before the UI forces you to design the interface from the consumer's perspective. It also enables:
- •Multiple frontends (web, mobile, third-party integrations) from day one
- •Independent deployment of frontend and backend
- •Automated testing of business logic without UI
REST vs GraphQL vs gRPC
| Protocol | Best For | Avoid When |
|---|---|---|
| REST | Standard CRUD operations, external APIs | Complex nested queries, real-time |
| GraphQL | Complex data requirements, mobile apps | Simple APIs, teams unfamiliar with it |
| gRPC | Internal service-to-service, high performance | External APIs, browser clients |
For most custom business applications, REST with OpenAPI specification is the right choice. It's well-understood, has excellent tooling, and is easy to integrate with third-party systems.
API Design Principles
Resource naming: Use nouns, not verbs
✅ GET /orders/{id}
❌ GET /getOrder/{id}Versioning: Version from day one
/api/v1/orders
/api/v2/orders (breaking changes go here)Pagination: Always paginate list endpoints
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 50,
"totalItems": 1247,
"totalPages": 25
}
}Error responses: Consistent error format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Order quantity must be greater than 0",
"field": "quantity",
"requestId": "uuid-for-support"
}
}3. Database Design for Business Applications
The Single Most Important Decision
Use a relational database (PostgreSQL) as your primary store. For 99% of custom business applications, the flexibility of NoSQL is not needed, and the ACID guarantees of a relational database are essential for financial and operational data.
Schema Design Principles
Normalise for writes, denormalise for reads
The operational database should be normalised (3NF) to prevent data anomalies. For reporting and dashboards, use materialised views or a separate read model.
Use UUIDs as primary keys
-- ✅ UUID primary key
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
...
);
-- ❌ Auto-increment integer
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
...
);UUIDs can be generated client-side (enabling optimistic UI), don't leak record counts to users, and work correctly in distributed systems.
Soft deletes for business records
-- Never hard-delete business records
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMPTZ;
-- "Deleted" records: WHERE deleted_at IS NOT NULL
-- Active records: WHERE deleted_at IS NULLAudit columns on every table
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- ... business columns ...
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES users(id)
);4. Event-Driven Architecture for Business Workflows
When to Use Events
Not every operation needs events. Use events when:
- •An action in one module should trigger actions in other modules
- •You need an audit trail of what happened and when
- •You want to decouple modules so they can evolve independently
Example: Order Fulfilment Workflow
OrderPlaced event →
InventoryService: reserve stock
WarehouseService: create pick list
NotificationService: send order confirmation to customer
FinanceService: create proforma invoice
OrderShipped event →
InventoryService: deduct stock
DeliveryService: create delivery record
FinanceService: create tax invoice
NotificationService: send shipping notificationImplementation: PostgreSQL LISTEN/NOTIFY
For SMB-scale applications, you don't need Kafka. PostgreSQL's built-in LISTEN/NOTIFY mechanism handles thousands of events per second and eliminates the operational complexity of a separate message broker.
-- Trigger that fires on order status change
CREATE OR REPLACE FUNCTION notify_order_status_change()
RETURNS TRIGGER AS $
BEGIN
PERFORM pg_notify(
'order_events',
json_build_object(
'event', 'ORDER_STATUS_CHANGED',
'orderId', NEW.id,
'oldStatus', OLD.status,
'newStatus', NEW.status
)::text
);
RETURN NEW;
END;
$ LANGUAGE plpgsql;5. Authentication and Authorisation
Authentication: Use a Proven Solution
Don't build authentication from scratch. Use:
- •Supabase Auth or Auth0 for new projects
- •Passport.js with JWT for Node.js projects that need custom flows
Authorisation: Role-Based Access Control (RBAC)
CREATE TABLE roles (id UUID PRIMARY KEY, name TEXT UNIQUE);
CREATE TABLE permissions (id UUID PRIMARY KEY, resource TEXT, action TEXT);
CREATE TABLE role_permissions (role_id UUID, permission_id UUID, PRIMARY KEY (role_id, permission_id));
CREATE TABLE user_roles (user_id UUID, role_id UUID, PRIMARY KEY (user_id, role_id));Check permissions in middleware, not in individual route handlers:
// Middleware
function requirePermission(resource, action) {
return async (req, res, next) => {
const hasPermission = await authService.checkPermission(
req.user.id, resource, action
);
if (!hasPermission) return res.status(403).json({ error: 'FORBIDDEN' });
next();
};
}
// Route
router.delete('/orders/:id',
requirePermission('orders', 'delete'),
orderController.delete
);6. Scalability Decisions
What You Actually Need at SMB Scale
Most Indian SMB custom applications need to handle:
- •50–500 concurrent users
- •10,000–1,000,000 records per table
- •100–10,000 API requests per minute
This is well within the capacity of a single well-configured server (4 vCPU, 16 GB RAM, SSD). You do not need microservices, Kubernetes, or distributed databases at this scale.
The Scalability Ladder
Start simple. Scale only when you have evidence you need to:
Level 1: Single server (handles 99% of SMB applications)
Level 2: Read replica for reporting queries
Level 3: Separate background job server
Level 4: CDN for static assets
Level 5: Horizontal scaling (only if Level 1–4 are insufficient)Caching Strategy
What to cache:
- Reference data (product catalogue, user roles) - Redis, 1-hour TTL
- Computed aggregates (dashboard metrics) - Redis, 5-minute TTL
- Session data - Redis
What NOT to cache:
- Transactional data (orders, inventory) - always read from DB
- User-specific data without proper cache key isolation7. Common Architectural Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| Modelling DB around UI screens | Every UI change requires schema migration | Model around business domain |
| No API versioning from day one | Breaking changes break all clients | Version from v1 |
| Hard deletes on business records | Cannot recover deleted data, audit trail gaps | Soft deletes with deleted_at |
| Building auth from scratch | Security vulnerabilities, wasted time | Use proven auth library |
| Premature microservices | Operational complexity without benefit | Start monolith, extract services when needed |
| No pagination on list endpoints | API times out as data grows | Always paginate |
Summary
Custom business software that lasts is built on domain-driven design, API-first architecture, a well-normalised relational database, and event-driven workflows for cross-module coordination. The most important architectural decisions are made in the first sprint - getting them right prevents the rewrites that cost 3× the original development budget.