1. The Problem
- TicketingHub is a booking platform used by tour operators, attractions, and activity providers worldwide.
- Who uses the availability calendar: Suppliers managing their day-to-day operations — checking bookings, assigning guides, spotting gaps.
- The pain: For suppliers with 10, 50, or 291 products, the availability page was unusable. It loaded everything — every product, every time slot, every day — in a single request.
- The numbers: Largest Core Web Vital (LCP) measured at 11.78 seconds. Some suppliers hit HTTP 500 errors because the URL exceeded length limits from passing 200+ product IDs.
- The root cause: The dashboard was calling its own API over HTTP — the frontend engine made a network roundtrip to a backend sitting in the same application. Serialization, middleware, retry logic — all for data that was one database query away.
2. The Design Hypothesis
- Insight: Nobody looks at 291 products at once. Operators care about subsets — "Downtown tours," "Weekend activities," "VIP experiences."
- Concept: Saved Views — let users create named collections of products and switch between them with tabs. Load only what the active view contains.
- Design goals:
- Zero-config first visit (it should just work)
- Fast view switching (tab-based, no page reload for selection)
- Minimal UI footprint (tabs, not a settings page)
- Familiar interaction patterns (Notion-like tabs, modal for creation)
3. The Design Decisions
- Default "All Products" view: Auto-created on first visit so existing users aren't greeted with an empty screen. Deletable if they want curated views only.
- Tab bar with inline delete: Each view is a tab with a product count badge. Delete via an X icon that appears on hover — confirmation dialog prevents accidents.
- Lazy-loaded creation modal: The product selection modal doesn't load until the user clicks "+ New View." For suppliers with hundreds of products, this keeps the base page fast.
- Modal UX details:
- Search filtering to find products quickly
- Select all / deselect all scoped to visible (filtered) results
- Selected state: light blue background, bold blue product name, larger checkbox
- Pinned header (name input, search) and footer (count, actions) with scrollable product list
- Real-time footer counter: "12 of 47 products"
- Empty state: When a supplier has zero products, a clean "No products yet" message instead of a broken calendar.
4. The Engineering
Before: For every page load.
Dashboard Controller
│
▼
Tickethub SDK (lib/tickethub/)
│
▼
HTTP Request
│
▼
Rack Middleware → Routing → API Controller
│
▼
JSON Serialization → Response
│
▼
SDK Parses JSON → OpenStruct Objects
│
▼
Dashboard Renders ViewAfter: One hop.
Dashboard Controller
│
▼
ActiveRecord Query → PostgreSQL
│
▼
Dashboard Renders ViewThree SDK calls replaced with direct queries
Result: Page loads dropped from 11+ seconds to under 2 seconds.
4a. The Architecture
- New AvailabilityView model with data integrity validation (product IDs must belong to the supplier)
- Manager pattern for CRUD (thin controllers, business logic in managers)
- Race condition handling on default view creation (find_or_create_by)
- Bootstrap Modal lifecycle management for dynamically injected content
4b. The Details That Ship Quality
- Full I18n across 24 locales
- Turbo Drive compatibility (disabled globally, so Rails UJS for confirmations)
- Duplicate form ID warnings resolved
- XHR edge case guarded (204 when no data, not a 500)
5. The Result
- Performance: ~80-85% reduction in page load time for large suppliers
- Reliability: No more 500 errors from URL length limits
- Usability: Operators can create focused views for their workflow — morning tours, weekend events, specific locations
- Code quality: Full test coverage (model, manager, request, service specs), clean architecture, zero linter warnings
Before — SDK HTTP calls:
3 separate HTTP roundtrips to the app's own API
availability = current_supplier.endpoint[:availability].get(from: from, to: to)
guide_shifts = current_supplier.endpoint[:guide_shifts].get(filter: { from: from, to: to })
products = current_supplier.endpoint[:products].getAfter — direct ActiveRecord queries:
availability = ProductAvailability.for_products(products, from, to)
guide_shifts = supplier.guide_shifts.where(starts_at: from..to).includes(:guide)
products = supplier.products.active.where(id: view.product_ids)Same data, same database, zero network hops.
6. Reflection
- Iteration is the process: The modal went through 10+ design iterations in a single development cycle — from basic list to search + select all + highlighted selection state. Each round was driven by actually using it.
- Performance is a design decision: The saved views feature wasn't just about organization — it was the mechanism that made performance possible. Design and engineering solved the same problem from different angles.
- Legacy systems are opportunities: The deprecated SDK wasn't just technical debt — it was the bottleneck users felt every day. Replacing it wasn't refactoring for its own sake; it was the single highest-impact change possible.