Home / Blog / Goodbye From Salon: Architectural Blueprint & System Design
Engineering Blueprint • Nov 6, 2026

Goodbye From Salon: Architectural Blueprint & System Design

Practical engineering guide and architectural blueprint for Goodbye From Salon: Architectural Blueprint & System Design.

UPTO 50% OFF
Trending:
BrickTry

AI is analyzing your requirement...

Generating custom modules, implementation options, and dynamic clarification questions.

Add Custom Requirement or Module

Add your own specific features, integrations, or components. AI will incorporate them to dynamically generate the next relevant options.

1. Progressive Clarifications

Click to expand & answer

2. Scope Modules & Features (/ Selected)

Click row to expand details · Customize options
✓
✕

Legacy appointment and resource-scheduling platforms—often instantiated as monolithic PHP/SQL scripts ("Salon" monoliths)—routinely hit structural performance walls when scaled to enterprise multi-location topologies. The combination of synchronous payment calls inside database transactions, table-level locks during peak booking windows, and tightly coupled notification pipes creates massive latency spikes, race conditions, and double-booking errors.

This engineering blueprint outlines the architectural decomposition and modernization of a monolithic salon/resource management system into a resilient, event-driven, multi-tenant scheduling engine. We will address zero-double-booking state synchronization, multi-tenant data isolation, asynchronous event processing, and real-time ledger consistency.


1. Deconstructing the Legacy Monolithic Architecture

In legacy legacy monolithic setups, a single HTTP request thread typically handles:

  1. Availability checks against a monolithic appointments table using unindexed WHERE NOT EXISTS or broad date range queries.
  2. Synchronous third-party payment gateway handshakes (e.g., Stripe, PayPal).
  3. Direct transactional writes mutating availability, customer CRM records, and inventory ledgers simultaneously.
  4. Synchronous SMS/Email notifications dispatched inside the main database transaction boundary.
[ HTTP Client ] ──( Synchronous Request )──► [ Monolithic Application Server ]
                                                   │
      ┌────────────────────────────────────────────┼────────────────────────────────────────────┐
      ▼                                            ▼                                            ▼
[ Database Transaction ]                [ External Payment API ]                     [ Notification API ]
(Table Lock: appointments)                (Latency: 400ms-1200ms)                      (Latency: 300ms-800ms)

Primary Structural Failure Modes

  • Cascading Latency: If the SMS gateway experiences a 2-second timeout, the database transaction remains open, holding locks on schedule slots and starving database connection pools.
  • Race Conditions & Phantom Bookings: High-concurrency events (e.g., a salon releasing weekend slots at noon) lead to dirty reads and simultaneous writes where two concurrent transactions read a slot as available before either commits.
  • Tightly Coupled State: Updating client loyalty points or updating inventory stock blocking the core appointment persistence path.

2. Decoupled Target System Architecture

The modernized system replaces the blocking monolith with an Event-Driven Modular Architecture backed by ephemeral Redis distributed locks, an append-only event store, and isolated asynchronous processing workers.

                                      ┌──────────────────────┐
                                      │   API Gateway /      │
                                      │   Edge Router        │
                                      └──────────┬───────────┘
                                                 │
                                                 ▼
                                      ┌──────────────────────┐
                                      │ Booking Orchestrator │
                                      └──────────┬───────────┘
                                                 │
            ┌────────────────────────────────────┼────────────────────────────────────┐
            │ (1. Ephemeral Lock)                │ (2. Atomic Commit)                 │ (3. Publish Event)
            ▼                                    ▼                                    ▼
┌───────────────────────┐            ┌───────────────────────┐            ┌───────────────────────┐
│ Redis Cluster         │            │ PostgreSQL Primary    │            │ Apache Kafka /        │
│ (Redlock Distributed) │            │ (Row-Level Security)  │            │ Redis Streams         │
└───────────────────────┘            └───────────────────────┘            └───────────┬───────────┘
                                                                                      │
                                                                   ┌──────────────────┴──────────────────┐
                                                                   ▼                                     ▼
                                                        ┌─────────────────────┐               ┌─────────────────────┐
                                                        │ Notification Worker │               │ Inventory & Ledger  │
                                                        └─────────────────────┘               └─────────────────────┘

3. High-Concurrency Booking Engine: Preventing Race Conditions

To eliminate double-bookings across concurrent client requests without relying on heavyweight database table locks, we implement a two-phase reservation protocol:

  1. Phase 1: Ephemeral Slot Allocation (Redis Distributed Lock): Acquire an atomic lock on the specific resource, staff member, and time-block tuple (resource_id:staff_id:slot_timestamp) using an aggressive TTL (e.g., 60 seconds).
  2. Phase 2: Database Transaction (PostgreSQL Isolation): Validate tenant boundaries, write the provisional appointment state, and emit a SlotReserved event.

Production Implementation: Distributed Slot Reservation Engine

The following TypeScript code demonstrates an enterprise-grade slot reservation service utilizing Redis atomic scripts and PostgreSQL transactional isolation.

// src/services/BookingEngine.ts
import { Redis } from 'ioredis';
import { Pool, PoolClient } from 'pg';

export interface ReservationPayload {
  tenantId: string;
  locationId: string;
  staffId: string;
  customerId: string;
  slotStartTime: Date;
  slotEndTime: Date;
  serviceIds: string[];
}

export interface ReservationResult {
  success: boolean;
  appointmentId?: string;
  errorCode?: 'SLOT_LOCKED' | 'SLOT_OCCUPIED' | 'TRANSACTION_FAILED';
}

export class DistributedBookingService {
  private redis: Redis;
  private dbPool: Pool;

  constructor(redisClient: Redis, dbPool: Pool) {
    this.redis = redisClient;
    this.dbPool = dbPool;
  }

  public async reserveSlot(payload: ReservationPayload): Promise<ReservationResult> {
    const slotKey = `lock:tenant:${payload.tenantId}:staff:${payload.staffId}:${payload.slotStartTime.getTime()}`;
    const lockTtlMs = 60000; // 60-second hold for checkout completion
    const lockAcquired = await this.acquireEphemeralLock(slotKey, payload.customerId, lockTtlMs);

    if (!lockAcquired) {
      return { success: false, errorCode: 'SLOT_LOCKED' };
    }

    const client: PoolClient = await this.dbPool.connect();

    try {
      await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');

      // Check for committed overlapping appointments using PostgreSQL tstzrange
      const overlapCheckQuery = `
        SELECT id FROM appointments
        WHERE tenant_id = $1
          AND staff_id = $2
          AND status IN ('CONFIRMED', 'PENDING')
          AND tsrange(start_time, end_time) && tsrange($3, $4)
        FOR UPDATE;
      `;

      const overlapResult = await client.query(overlapCheckQuery, [
        payload.tenantId,
        payload.staffId,
        payload.slotStartTime.toISOString(),
        payload.slotEndTime.toISOString(),
      ]);

      if (overlapResult.rows.length > 0) {
        await client.query('ROLLBACK');
        await this.releaseEphemeralLock(slotKey, payload.customerId);
        return { success: false, errorCode: 'SLOT_OCCUPIED' };
      }

      // Insert provisional appointment record
      const insertQuery = `
        INSERT INTO appointments (
          tenant_id, location_id, staff_id, customer_id,
          start_time, end_time, status, lock_key
        ) VALUES ($1, $2, $3, $4, $5, $6, 'PENDING', $7)
        RETURNING id;
      `;

      const insertResult = await client.query(insertQuery, [
        payload.tenantId,
        payload.locationId,
        payload.staffId,
        payload.customerId,
        payload.slotStartTime.toISOString(),
        payload.slotEndTime.toISOString(),
        slotKey,
      ]);

      const appointmentId = insertResult.rows[0].id;

      // Stage event for outbox processor (Transactional Outbox Pattern)
      const outboxQuery = `
        INSERT INTO event_outbox (tenant_id, event_type, payload)
        VALUES ($1, 'APPOINTMENT_RESERVED', $2);
      `;
      await client.query(outboxQuery, [
        payload.tenantId,
        JSON.stringify({ appointmentId, ...payload }),
      ]);

      await client.query('COMMIT');
      return { success: true, appointmentId };

    } catch (error) {
      await client.query('ROLLBACK');
      await this.releaseEphemeralLock(slotKey, payload.customerId);
      console.error('Execution error during slot reservation:', error);
      return { success: false, errorCode: 'TRANSACTION_FAILED' };
    } finally {
      client.release();
    }
  }

  private async acquireEphemeralLock(key: string, value: string, ttlMs: number): Promise<boolean> {
    const result = await this.redis.set(key, value, 'PX', ttlMs, 'NX');
    return result === 'OK';
  }

  private async releaseEphemeralLock(key: string, value: string): Promise<void> {
    // Lua script guarantees atomic check-and-delete
    const luaScript = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;
    await this.redis.eval(luaScript, 1, key, value);
  }
}

4. Architectural Layering & Benchmarks

Comparing the monolithic "Salon" legacy setup against the modernized, decoupled target architecture illustrates key shifts in operational characteristics and throughput metrics:

Vector Legacy Monolithic "Salon" Script Modernized Event-Driven Architecture
Concurrency Lock Table/Database Locks (SELECT ... FOR UPDATE across broad scopes) Atomic Ephemeral Redis Distributed Locks + Narrow Row Locks
P99 Booking Latency 1,450ms (Synchronous Payment & Notification execution) 42ms (Asynchronous Checkout Execution & Outbox Dispatch)
Data Isolation Shared Single-Tenant Tables without explicit schema enforcement Multi-Tenant Row-Level Security (RLS) with explicit Tenant Isolation
Failure Scope Single API/Integration failure halts entire application thread Dead-Letter Queues (DLQ) isolate failures to retryable downstream micro-workers
Notification Pipeline Blocking inline SMTP/Twilio HTTP execution Decoupled Async Event Bus (Kafka/RabbitMQ/Redis Streams)
Max Throughput ~45 bookings/sec (Monolithic DB contention limit) ~3,800 bookings/sec (Horizontal Scalable API Nodes + Redis Lock Engine)

5. Event-Driven Pipeline: Async Event Processor with Exponential Backoff

To prevent third-party notification or payment gateway failures from impacting the system, we implement an Outbox Consumer. This worker fetches events from the event_outbox table, delivers payloads, and handles failures via exponential backoff and Dead-Letter Queue (DLQ) routing.

The following Go micro-worker safely processes pending system events:

// cmd/worker/outbox_processor.go
package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"time"

	_ "github.com/lib/pq"
)

type OutboxEvent struct {
	ID        string          `json:"id"`
	TenantID  string          `json:"tenant_id"`
	EventType string          `json:"event_type"`
	Payload   json.RawMessage `json:"payload"`
	Attempts  int             `json:"attempts"`
}

type EventProcessor struct {
	db *sql.DB
}

func NewEventProcessor(db *sql.DB) *EventProcessor {
	return &EventProcessor{db: db}
}

func (ep *EventProcessor) ProcessOutboxBatch(ctx context.Context, batchSize int) error {
	tx, err := ep.db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("failed to begin transaction: %w", err)
	}
	defer tx.Rollback()

	// Claim events using SKIP LOCKED to allow multiple parallel worker processes
	query := `
		SELECT id, tenant_id, event_type, payload, attempts
		FROM event_outbox
		WHERE status = 'PENDING' AND next_retry_at <= NOW()
		ORDER BY created_at ASC
		LIMIT $1
		FOR UPDATE SKIP LOCKED;
	`

	rows, err := tx.QueryContext(ctx, query, batchSize)
	if err != nil {
		return fmt.Errorf("failed to fetch events: %w", err)
	}
	defer rows.Close()

	var events []OutboxEvent
	for rows.Next() {
		var ev OutboxEvent
		if err := rows.Scan(&ev.ID, &ev.TenantID, &ev.EventType, &ev.Payload, &ev.Attempts); err != nil {
			return err
		}
		events = append(events, ev)
	}

	for _, event := range events {
		if err := ep.dispatch(ctx, event); err != nil {
			ep.handleFailure(ctx, tx, event, err)
		} else {
			ep.handleSuccess(ctx, tx, event.ID)
		}
	}

	return tx.Commit()
}

func (ep *EventProcessor) dispatch(ctx context.Context, event OutboxEvent) error {
	// Simulate dispatch to external notification/webhook broker
	log.Printf("[Event %s] Dispatching %s for Tenant %s", event.ID, event.EventType, event.TenantID)

	// Implementation-specific logic (e.g., HTTP POST to notification microservice)
	return nil
}

func (ep *EventProcessor) handleSuccess(ctx context.Context, tx *sql.Tx, eventID string) {
	query := `UPDATE event_outbox SET status = 'PROCESSED', processed_at = NOW() WHERE id = $1;`
	_, _ = tx.ExecContext(ctx, query, eventID)
}

func (ep *EventProcessor) handleFailure(ctx context.Context, tx *sql.Tx, event OutboxEvent, dispatchErr error) {
	maxAttempts := 5
	nextAttempts := event.Attempts + 1

	if nextAttempts >= maxAttempts {
		log.Printf("[DLQ] Event %s exceeded max retries. Routing to DLQ. Error: %v", event.ID, dispatchErr)
		query := `UPDATE event_outbox SET status = 'FAILED', last_error = $1 WHERE id = $2;`
		_, _ = tx.ExecContext(ctx, query, dispatchErr.Error(), event.ID)
		return
	}

	// Calculate exponential backoff delay: 2^attempts * 5 seconds
	backoffSeconds := time.Duration(math.Pow(2, float64(nextAttempts))) * 5 * time.Second
	nextRetry := time.Now().Add(backoffSeconds)

	query := `
		UPDATE event_outbox
		SET attempts = $1, next_retry_at = $2, last_error = $3
		WHERE id = $4;
	`
	_, _ = tx.ExecContext(ctx, query, nextAttempts, nextRetry, dispatchErr.Error(), event.ID)
}

func main() {
	// Worker entrypoint initialization omitted for brevity
}

6. Enterprise Multi-Tenant Data Security Strategy

When migrating away from legacy monolithic scripts, multi-tenant isolation is enforced at the database level using PostgreSQL Row-Level Security (RLS). This prevents cross-tenant data leaks even if an API layer bug occurs.

-- Migration: Enable Multi-Tenant Row Level Security (RLS)

-- 1. Enable RLS on core tables
ALTER TABLE appointments ENABLE ROW LEVEL SECURITY;
ALTER TABLE clients ENABLE ROW LEVEL SECURITY;
ALTER TABLE staff_schedules ENABLE ROW LEVEL SECURITY;

-- 2. Create tenant enforcement policy using session variables
CREATE POLICY tenant_isolation_policy ON appointments
    FOR ALL
    USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);

CREATE POLICY tenant_isolation_policy ON clients
    FOR ALL
    USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);

-- 3. Application Connection Pool Initialization Hook
-- Executed by middleware on every checkout connection from the pool:
-- SET LOCAL app.current_tenant_id = 'c39a5f82-2b6e-4e89-8d83-bc2a1e827103';

## How BrickTry Helps You Build and Scale This

Decomposing high-concurrency legacy systems into production-ready distributed architectures presents real operational challenges. BrickTry accelerates system modernization and migration from legacy monolithic scripts into scalable cloud architectures.

  • Interactive In-Browser Lab Sandbox (/lab): Prototype and evaluate distributed locking algorithms, event processors, and complex multi-tenant database schemas in real time. The BrickTry Lab provides a zero-setup, in-browser containerized environment to stress-test race conditions and memory usage before writing production code.
  • Autonomous AI Dev Pairing Agents: Scaffolding complex transactional outbox layers, TypeScript models, or Go microservices is automated using BrickTry’s domain-aware AI pairing engine. AI agents analyze existing legacy schema structures, execute automated AST refactoring, and generate type-safe API boundaries and database migrations.
  • Dedicated Senior Full-Stack Engineering Pods: System design blueprints are reviewed and deployed alongside experienced platform architects. BrickTry’s senior engineers conduct strict performance audits, implement load-testing vectors (simulating thousands of concurrent slot reservations), and establish robust CI/CD deployment pipelines.
  • 100% Intellectual Property & Source Code Ownership: Every line of code, Docker container config, and Terraform script created through BrickTry is committed directly to your private GitHub repository with full source ownership and zero vendor lock-in.

Frequently Asked Questions

1. How does this design handle a situation where a user acquires a Redis lock but drops their internet connection before completing checkout?

The Redis lock is provisioned with a strict Time-To-Live (TTL)—typically 60 to 180 seconds. If the client drops offline, the transaction is never finalized in PostgreSQL. The Redis lock key automatically expires, restoring slot availability without manual database cleanup or administrative intervention.

2. Why use the Transactional Outbox Pattern instead of publishing directly to Kafka/RabbitMQ inside the API endpoint?

Publishing directly to an external message broker inside an HTTP request handler creates a dual-write problem. If the database commit succeeds but the network connection to the message broker fails (or vice versa), your state becomes inconsistent. The Transactional Outbox Pattern commits the event to PostgreSQL within the same database transaction as the appointment record. The separate outbox processor then reliably guarantees at-least-once message delivery to the message broker.

3. Will PostgreSQL Row-Level Security (RLS) introduce significant latency overhead at scale?

When configured with appropriate compound indexes (e.g., CREATE INDEX idx_appointments_tenant_staff ON appointments(tenant_id, staff_id)), the performance overhead of PostgreSQL RLS is minimal (typically <1% query execution cost). RLS executes directly inside the query execution engine, guaranteeing that tenant filters are enforced before index scanning occurs.

Build, Test, and Scale This on BrickTry

BrickTry pairs you with autonomous AI scaffolding supervised by dedicated senior full-stack software engineers in an interactive in-browser development sandbox (/lab). Test, build, and deploy production-grade software with 100% source code ownership and zero vendor lock-in.

Launch Interactive Requirement Builder →

❤️

Support BrickTry Platform & Engineering Development

Help us build, maintain, and advance our AI engineering platform. Every donation fuels open-source tooling, infrastructure, and continuous improvements.

$
Donor Details
Promote Your Brand / Link Wall

UPI / Credit & Debit Cards / Netbanking
Razorpay
Secure 256-bit encrypted checkout
View Leaderboard & Wall

Hey!

Welcome, Let's chat —
start a new conversation
below.

Recent conversations
See all

We’re online to assist you with your project...

Abhishek A Agrawal • Just now

Start a conversation

Quick contact setup

Please share your details below so our team can reach you.

Worldwide supported

🔒 Your info is only used to connect with our support team.

Abhishek A Agrawal

Online & Ready to Assist

BrickTry

Select a Date & Time Enter Details You are Scheduled

Technical Consultation

Video link provided upon confirmation
SUN
MON
TUE
WED
THU
FRI
SAT
Time zone:

,

•

Google Meet
Default HD Video
Calendar invitations with direct video links will be sent here.

You are scheduled

A calendar invitation has been sent to your email address.