SaaS Architecture

Lessons Learned Building SaaS Products

Published on March 10, 2026 • 10 Min Read • Written by Bhuvanesh V

Introduction

Building software products for client use is a challenging task. Unlike personal sandbox projects (where developers can adjust code bases as they go), client projects require clear scope definitions, data integrity validations, and reliable deployments.

During my work on client applications like the Mobile Service Management system and the ETS2 mod distribution DRM, I learned key lessons regarding relational database designs, payment tracking, and handling integration failures. In this article, I will share the takeaways I gathered while building client SaaS systems.

Scope Definition and MVP Focus

The single most common cause of client project failure is scope creep. Clients frequently identify new features during the development cycle, which can delay the initial launch date.

To prevent this, we enforce a strict Minimum Viable Product (MVP) boundary:

  • Define Core Value: Identify the minimum set of features required to solve the primary business problem (e.g., generating repair tickets).
  • Postpone Nice-to-Have Features: Route additional requests (like bulk exports, advanced analytics charts, or custom integrations) to a Phase 2 backlog.
  • Iterative Releases: Deliver the MVP first, allowing staff members to test the core flows in a production environment before introducing complexity.

Database Integrity & Cascades

When building backend databases in Supabase or vanilla PostgreSQL, data constraints must be enforced at the engine level rather than relying entirely on frontend validation.

For example, if a customer is deleted, what happens to their active repair tickets? If the database schema lacks foreign key constraints, those tickets remain as orphan records, causing dashboard query failures when trying to access missing customer relationships.

We prevent this by defining explicit foreign key behaviors:

CREATE TABLE tickets (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_id uuid NOT NULL,
  device_model text NOT NULL,
  
  -- Restrict customer deletion if they have active tickets
  CONSTRAINT fk_customer
    FOREIGN KEY(customer_id) 
    REFERENCES customers(id)
    ON DELETE RESTRICT
);

Financial Logging: Immutable Ledger Pattern

A common mistake when tracking finances in a CRM is updating a single balance column on the user record. If a cashier updates the balance manually without an audit trail, the business loses the ability to verify historical transactions or reconcile balances.

To address this, we use an immutable ledger pattern. Instead of updating a balance value directly, we append a new transaction row to a ledger table. The current balance is then calculated dynamically by summing the historical transaction values.

Below is the ledger query pattern:

-- Calculate the current pending balance for a specific ticket
SELECT 
  t.price_estimate - COALESCE(SUM(l.amount), 0) AS pending_balance
FROM tickets t
LEFT JOIN ledger l ON l.ticket_id = t.id
WHERE t.id = 'target-ticket-uuid'
GROUP BY t.id, t.price_estimate;

Handling Integration Failures

SaaS systems frequently depend on third-party services like Resend (for emails), Stripe (for payments), or Twilio (for SMS). If these external services experience downtime, the application must handle the failure gracefully.

If a customer's repair is marked as completed but the SMS alert service is temporarily down, the database transaction should still succeed. We decouple critical database updates from external API calls by implementing retry queues or using background processes to manage notifications separately, ensuring that network issues do not block core business operations.

Conclusion

Building SaaS products for clients requires shifting focus from simple code execution to system reliability. Enforcing strict database constraints, implementing immutable ledgers for financial tracking, and handling integration failures gracefully are key to delivering reliable software solutions.