DEV Community

DEEKSHITHA A
DEEKSHITHA A

Posted on

Design Patterns Behind QueueLess: How We Built a Real-Time Virtual Queue System

Design Patterns Behind QueueLess: How We Built a Real-Time Virtual Queue System

A technical walkthrough of the Observer, Adapter, and Factory patterns as applied in QueueLess, an AI-powered virtual queue and appointment management platform.


Introduction

QueueLess lets customers book a virtual token at a nearby business, track their position in real time, and get notified as their turn approaches — without ever standing in a physical line. On the other side, business staff and admins manage the queue, call the next customer, and keep an eye on daily performance from a dashboard.

Under the hood, three classic design patterns quietly do most of the heavy lifting: the Observer pattern keeps everyone in sync with the live queue, the Adapter pattern lets us plug in any notification provider without touching business logic, and the Factory pattern decides which notifier to use in the first place. This post walks through each one, why we needed it, and how they fit together.


1. The Observer Pattern — Real-Time Queue Notifications

The Problem

When a token is served and the queue advances, multiple things need to happen at once: the customer's app should update its position indicator, a push notification should fire if they're close to the front, and the admin dashboard should reflect the new state. Hard-coding all of that inside the Queue class would tightly couple queue logic to notification logic and UI logic — a maintenance nightmare the moment we add a new kind of listener.

The Solution

The Observer pattern decouples the Queue (the thing that changes) from everything that needs to react to that change. Queue doesn't know or care what a CustomerApp or a PushNotifier actually does — it just calls update() on whoever is registered.

class Observer:
    def update(self, message):
        pass

class CustomerApp(Observer):
    def update(self, message):
        print("CustomerApp:", message)

class PushNotifier(Observer):
    def update(self, message):
        print("Push Notification:", message)

class Queue:
    def __init__(self):
        self.observers = []

    def add_observer(self, observer):
        self.observers.append(observer)

    def notify(self, message):
        for observer in self.observers:
            observer.update(message)
Enter fullscreen mode Exit fullscreen mode

How It Works in QueueLess

Queue
  ↓  "Position updated → Token #24, ETA 5 min"
Observers receive notification:
  • CustomerApp updates the live position screen
  • PushNotifier sends a push notification
  • Admin Dashboard updates the live queue view
  • Analytics Logger stores the event for daily reports
Enter fullscreen mode Exit fullscreen mode

Adding a new kind of listener — say, an SMS reminder or a kiosk display — never requires touching Queue. We just implement Observer and call add_observer().

Benefits

  • Loose coupling between the queue and everything watching it
  • New observers can be added without modifying existing code (open/closed principle)
  • Every observer gets the update at the same time, so the customer app and dashboard never drift out of sync

2. The Adapter Pattern — Unifying Notification Channels

The Problem

"Notify the customer" sounds simple until you realize how varies wildly: Firebase Cloud Messaging expects one API shape, a third-party SMS gateway expects another (sendSms(phone, text)), and email is different again. If NotificationService called each provider's API directly, every provider change would ripple through the whole codebase.

The Solution

The Adapter pattern wraps each provider behind one common interface, NotificationSender, so the rest of the app only ever calls .send(message) — regardless of what's happening underneath.

class NotificationSender:
    def send(self, message):
        pass

class SMSAdapter(NotificationSender):
    def __init__(self, sms_gateway):
        self.sms_gateway = sms_gateway   # the mismatched third-party API

    def send(self, message):
        self.sms_gateway.sendSms(self.phone, message)

class SmsGatewayAPI:
    def sendSms(self, phone, text):
        print("Sending SMS:", text)
Enter fullscreen mode Exit fullscreen mode

NotificationService (the client) only ever depends on NotificationSender. It has no idea SmsGatewayAPI even exists — that detail is hidden entirely inside SMSAdapter.

Benefits

  • Swap or add notification providers without changing NotificationService
  • Third-party API quirks stay contained in one adapter class instead of leaking everywhere
  • Makes the notification layer easy to unit-test with mock adapters

3. The Factory Pattern — Creating the Right Notifier

The Problem

Even with a clean NotificationSender interface, something still has to decide which concrete adapter to instantiate — PushAdapter, SMSAdapter, or EmailAdapter — based on the customer's preferred channel. Scattering that decision (if channel == "sms": ... elif channel == "push": ...) across the codebase is exactly the kind of duplication design patterns exist to prevent.

The Solution

NotificationFactory centralizes that decision in one place.

class NotificationFactory:
    @staticmethod
    def create_notifier(channel):
        if channel == "push":
            return PushAdapter()
        elif channel == "sms":
            return SMSAdapter(SmsGatewayAPI())
        elif channel == "email":
            return EmailAdapter()
        else:
            raise ValueError(f"Unknown channel: {channel}")
Enter fullscreen mode Exit fullscreen mode

NotificationService never touches PushAdapter or SMSAdapter by name — it just says:

notifier = NotificationFactory.create_notifier(customer.preferred_channel)
notifier.send("Your turn is next! Token #24, Counter 3")
Enter fullscreen mode Exit fullscreen mode

Benefits

  • One place to update when a new channel is added — not a dozen scattered conditionals
  • NotificationService depends only on the NotificationSender interface, never on concrete classes
  • Makes it trivial to default, fall back, or A/B test between channels

How the Three Patterns Work Together

These aren't three isolated exercises — they form a pipeline:

  1. Factory decides which notifier to build (NotificationFactory.create_notifier("push"))
  2. Adapter makes that notifier speak the app's common language (PushAdapter implements NotificationSender)
  3. Observer decides when to call it (Queue.notify() fires whenever the position changes)
Queue.notify()  →  loops through observers  →  PushNotifier.update()
                                                       │
                                     NotificationFactory.create_notifier("push")
                                                       │
                                                 PushAdapter.send()
Enter fullscreen mode Exit fullscreen mode

A customer's queue position changes → the Observer pattern broadcasts it → the Factory hands back the right channel-specific object → the Adapter translates that into a call the underlying provider actually understands.


Performance Metrics of the Three Patterns

Metric Observer Adapter Factory
Scalability High High High
Maintainability Excellent Excellent Excellent
Code Reusability High High High
Flexibility Supports multiple observer/notification modules Supports multiple notification channels (Push, SMS, Email) Supports adding new channels with one new branch/class
Coupling Low Low Low
Extensibility Easy to add new observers Easy to add new channel adapters Easy to add new notifier types

Conclusion

None of these patterns exist for their own sake — each one solves a concrete problem QueueLess actually has: keeping many parts of the app in sync (Observer), dealing with incompatible third-party notification APIs (Adapter), and centralizing which notifier gets built (Factory). Used together, they let us add a new business type, a new notification channel, or a new kind of live-update listener without rewriting the core queue logic — which is really the whole point of a design pattern: not cleverness for its own sake, but code that's easy to extend and hard to break.

Top comments (0)