← Resources/GuideCustom Solutions

Custom Software Architecture: How to Design Systems That Scale Without Rewriting

7 April 2026·18 min read

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, Ledger

The 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

ProtocolBest ForAvoid When
RESTStandard CRUD operations, external APIsComplex nested queries, real-time
GraphQLComplex data requirements, mobile appsSimple APIs, teams unfamiliar with it
gRPCInternal service-to-service, high performanceExternal 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

json
{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 50,
    "totalItems": 1247,
    "totalPages": 25
  }
}

Error responses: Consistent error format

json
{
  "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

sql
-- ✅ 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

sql
-- 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 NULL

Audit columns on every table

sql
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 notification

Implementation: 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.

sql
-- 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)

sql
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:

javascript
// 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 isolation

7. Common Architectural Pitfalls

PitfallConsequenceFix
Modelling DB around UI screensEvery UI change requires schema migrationModel around business domain
No API versioning from day oneBreaking changes break all clientsVersion from v1
Hard deletes on business recordsCannot recover deleted data, audit trail gapsSoft deletes with deleted_at
Building auth from scratchSecurity vulnerabilities, wasted timeUse proven auth library
Premature microservicesOperational complexity without benefitStart monolith, extract services when needed
No pagination on list endpointsAPI times out as data growsAlways 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.

software architectureDDDAPI-firstevent-drivenmicroservicescustom development