[Feature] SPORTSHUB Forms

[Feature] SPORTSHUB Forms

September 30, 2025ยทBrian Wang
Brian Wang

Forms Architecture

Building SPORTSHUB Forms ๐Ÿ“

One of SPORTSHUB’s most powerful features is our Forms system โ€“ allowing event organizers to collect custom information from attendees during the ticket purchase process. But what makes this feature truly special isn’t just what it does for users, but how we built it by fundamentally rethinking our payment architecture.

What Are SPORTSHUB Forms?

SPORTSHUB Forms transform the ticket purchasing experience by allowing event organizers to get the necessary information they need from attendees โ€“ all in one smooth, integrated workflow. Instead of juggling separate surveys, emails, and spreadsheets, organizers can gather everything from dietary preferences to emergency contacts right during checkout.

Forms Gallery

What Makes Forms Great for Event Organizers:

๐ŸŽฏ Collect Any Information You Need

  • Gather dietary restrictions, t-shirt sizes, skill levels, team preferences
  • Collect emergency contacts, medical information, or accessibility needs

โšก Seamless Ticket Purchase Experience

  • No separate forms to send after payment โ€“ everything happens during checkout
  • Attendees complete forms as part of buying tickets, ensuring 100% completion rates
  • Each ticket can have its own custom information requirements
  • Form links are shareable, meaning attendees can easily send a form to other attendees to complete

๐Ÿ”ง Flexible Form Building

  • Choose from text input, multiple choice/dropdown and image sections
  • Set required fields to ensure you get critical information
  • Real-time validation prevents errors and incomplete submissions

๐Ÿ“Š Organized Data Collection

  • All responses automatically organized by event and ticket
  • Export responses to CSV for easy analysis and planning
  • No more chasing attendees for missing information after they’ve paid

Forms Example

The Technical Challenge: Generalizing Payment Workflows

Initially, SPORTSHUB had a simple ticket purchasing flow:

Select tickets โ†’ Pay with Stripe โ†’ Done.

But forms introduced a new complexity: we needed a multi-step workflow that could handle different types of interactions before, during, and after payment.

From Simple to Multi-Step

// Before: Simple ticket purchase
EventPayment โ†’ StripeCheckout โ†’ Success

// After: Multi-step workflow with forms
EventPayment โ†’ Form โ†’ Form โ†’ StripePayment โ†’ Success

This required us to completely rethink our payment architecture and led to the creation of our Fulfilment Session system.

Forms Workflow

Fulfilment Sessions: The Foundation

We realized we needed to abstract the concept of “completing a purchase” into a series of fulfilment entities that could be processed in sequence.

The beauty of this system lies in its modular design. Each fulfillment entity is completely independent and interchangeable, allowing us to easily swap components or add new functionality without touching existing code. Need to switch from Stripe to PayPal? Simply replace the STRIPE entity. Want to add a waiver signing step? Insert a WAIVER entity before payment. This plug-and-play approach gives us incredible flexibility to customize the user journey for different event types while maintaining a clean, maintainable codebase.

The Core Concept

public enum FulfilmentEntityType {
    FORMS,    // Collect custom information
    STRIPE,   // Process payment
    END       // Complete the transaction
}

Each fulfilment session contains an ordered list of entities that must be completed:

// Example: Event with form โ†’ payment โ†’ completion
List<FulfilmentEntity> entities = [
    FormsFulfilmentEntity,  // Collect attendee info
    StripeFulfilmentEntity, // Process payment
    EndFulfilmentEntity     // Redirect to success page
]

Multi-Ticket Forms

For events requiring multiple tickets, we create one form entity per ticket:

// 3 tickets = 3 form entities + 1 payment + 1 end
List<FulfilmentEntity> entities = [
    FormsFulfilmentEntity, // Ticket 1 info
    FormsFulfilmentEntity, // Ticket 2 info  
    FormsFulfilmentEntity, // Ticket 3 info
    StripeFulfilmentEntity, // Single payment
    EndFulfilmentEntity     // Success
]

From Stateful to Stateless: A Design Evolution

The Stateful Approach (v1)

Initially, we built a stateful system where the backend tracked user progress:

// Backend maintains state
class FulfilmentSession {
    private int currentEntityIndex;
    private String currentUserId;
    private Map<String, FulfilmentEntity> entities;
    
    public FulfilmentEntity getCurrentEntity() {
        return entities.get(currentEntityIndex);
    }
}

Problems with this approach:

  • Complex state management
  • Race conditions between multiple browser tabs
  • Inability to accurately manage state when users are redirected to external sites
  • Difficult error recovery
  • Server-side session tracking overhead

The Stateless Approach (v2)

We realized a much simpler approach: if a user has a link to a specific fulfilment entity, they’re on that step. The URL becomes the state.

// URL structure defines the current state
/fulfilment/{sessionId}/{entityId}

// No server-side tracking needed!
// The URL tells us exactly where the user is

Benefits of stateless design:

  • Simplicity: No complex state tracking
  • Reliability: Users can bookmark, refresh, or share links
  • Scalability: No server-side session storage
  • Multi-device: Same URL works across devices
  • Error Recovery: Users can always return to their exact position

Implementation Details

public class FulfilmentService {
    // Stateless entity info retrieval
    public GetFulfilmentEntityInfoResponse getFulfilmentEntityInfo(
        String fulfilmentSessionId, 
        String fulfilmentEntityId
    ) {
        // No session tracking - just return entity details
        FulfilmentEntity entity = getEntityById(fulfilmentEntityId);
        return createResponse(entity);
    }
}

The frontend handles navigation between entities by constructing URLs:

// Navigate to next entity
const nextEntityId = await getNextFulfilmentEntity(sessionId, currentEntityId);
router.push(`/fulfilment/${sessionId}/${nextEntityId}`);

The GlobalAppController: Unifying Our Architecture

As our fulfilment and forms functionality grew, we faced another challenge: endpoint proliferation. We had separate cloud functions for:

  • initFulfilmentSession
  • getFulfilmentEntityInfo
  • getNextFulfilmentEntity
  • saveTempFormResponse
  • updateFulfilmentEntity
  • …and many more

The Problem: Cold Starts and Complexity

Each separate endpoint meant:

  • Multiple cold starts: Each function had to warm up independently
  • Resource inefficiency: Couldn’t share minimum instances across functions
  • Deployment complexity: Managing dozens of individual functions
  • Inconsistent error handling: Different patterns across functions

The Solution: One Controller to Rule Them All

We created the GlobalAppController โ€“ a unified endpoint that routes all requests internally:

@HttpFunction
public class GlobalAppController implements HttpFunction {
    
    public void service(HttpRequest request, HttpResponse response) {
        UnifiedRequest unifiedRequest = parseRequest(request);
        Object result = routeRequest(unifiedRequest);
        sendResponse(response, result);
    }
    
    private Object routeRequest(UnifiedRequest request) {
        EndpointType endpointType = request.endpointType();
        Handler handler = HandlerRegistry.getHandler(endpointType);
        return handler.handle(handler.parse(request));
    }
}

Type-Safe Routing

The magic happens in our EndpointType enum and HandlerRegistry:

public enum EndpointType {
    SAVE_TEMP_FORM_RESPONSE(SaveTempFormResponseRequest.class, SaveTempFormResponseResponse.class),
    INIT_FULFILMENT_SESSION(InitCheckoutFulfilmentSessionRequest.class, InitCheckoutFulfilmentSessionResponse.class),
    GET_FULFILMENT_ENTITY_INFO(GetFulfilmentEntityInfoRequest.class, GetFulfilmentEntityInfoResponse.class),
    // ... all other endpoints
}

public class HandlerRegistry {
    private static final Map<EndpointType, Handler<?, ?>> handlers = new HashMap<>();
    
    static {
        handlers.put(SAVE_TEMP_FORM_RESPONSE, new SaveTempFormResponseHandler());
        handlers.put(INIT_FULFILMENT_SESSION, new InitFulfilmentSessionHandler());
        handlers.put(GET_FULFILMENT_ENTITY_INFO, new GetFulfilmentEntityInfoHandler());
        // ... register all handlers
    }
}

Frontend Integration

The frontend sends all requests to a single endpoint via a central executeGlobalAppControllerFunction method, ensuring our code remains modular to easily add any new functionalities on the GlobalAppController.

The Benefits

Performance Improvements:

  • Reduced cold starts: Single function stays warm longer
  • Efficient resource utilization: One minimum instance handles all traffic
  • Better connection pooling: Shared database connections

Development Benefits:

  • Unified error handling: Consistent patterns across all endpoints
  • Easier testing: Mock one controller instead of many functions
  • Simplified deployment: Single function to deploy and monitor
  • Type safety: Compile-time checks for request/response types

The Result: A Powerful, Scalable System

The combination of stateless fulfilment sessions and the GlobalAppController created a system that is:

For Users:

  • Seamless: Forms feel like a natural part of checkout
  • Reliable: Can resume from any point without losing progress
  • Flexible: Supports complex multi-step workflows

For Developers:

  • Maintainable: Clear separation of concerns
  • Scalable: Stateless design handles any load
  • Extensible: Easy to add new fulfilment entity types

For Infrastructure:

  • Efficient: Reduced cold starts and better resource utilization
  • Simple: One endpoint to monitor and maintain
  • Cost-effective: Optimized minimum instance allocation

Looking Forward

The architecture we built for Forms has become the foundation for other SPORTSHUB features. The fulfilment session pattern is now used for:

  • Event creation workflows
  • User onboarding flows
  • Payment processing
  • Multi-step operations

And the GlobalAppController pattern has simplified our entire backend architecture, making it easier to add new features while maintaining performance and reliability.


The SPORTSHUB Forms feature represents a perfect example of how good architecture decisions compound over time. By thinking deeply about the underlying patterns and building flexible, reusable systems, we created something that works beautifully for users while being a joy to work with as developers.

Want to try SPORTSHUB Forms? Create an event and add a custom form to see the magic in action! ๐Ÿš€

Last updated on