Case study · reservatiesoftware · gebruikerservaring

Snellere reserveringswidget: minder wachttijd bij het boeken

De reserveringswidget werkte, maar voelde traag aan bij herhaald gebruik — niet door slechte hardware, maar doordat dezelfde gegevens bij elke klik opnieuw werden opgehaald. De oplossing: slim hergebruiken wat stabiel is, gericht ophalen wat verandert.

Klantervaring

Trage widget

Klikken op datum, personen of tijdslot voelde traag aan — bij elke interactie werd alles opnieuw geladen.

Aanpak

Slimmer, niet meer

Dezelfde servers, minder werk per klik. Stabiele data hergebruiken, variabele data gericht en snel ophalen.

Resultaat

Directe respons

De widget reageert consistent bij herhaald klikken — ook bij drukke boekingsmomenten.

Probleem

Per interactie werd te veel opnieuw tegen de database gevalideerd

In de widget zagen we dat dezelfde datum meerdere keren opnieuw gevalideerd werd. Wie een dag opende, één keer van aantal personen wisselde of naar een ander tijdslot klikte, forceerde telkens opnieuw dezelfde onderliggende controles.

Wat dat concreet betekende

  • Shifts ophalen.
  • Openingsstatus controleren.
  • Sluitingsdagen checken.
  • Capaciteit per tijdslot bepalen.

Analyse

Stabiele data werd behandeld alsof ze elke request opnieuw moest bestaan

Stabiele data zoals shifts per restaurant en weekdag werd telkens opnieuw geladen.

Openingsstatus en sluitingsdagen werden herhaaldelijk gevalideerd in dezelfde gebruikersflow.

Capaciteit per tijdslot werd opgevraagd op een manier die lineair meer databasecalls veroorzaakte.

De widget werkte functioneel, maar betaalde die correctheid met onnodig veel werk per request.

De kern van de oplossing zat in een heldere opsplitsing: stabiele data naar cache, volatiele data kort cachen of direct ophalen. Daarmee werd het gedrag niet alleen sneller, maar ook beter voorspelbaar.

Oplossing

Twee typen data, twee verschillende aanpakken

We hebben de widget op twee assen herwerkt: configuratiedata wordt hergebruikt vanuit cache, terwijl beschikbare tijdsloten gericht en kort worden opgehaald.

Stap 1 — hergebruik wat stabiel is

Shifts per restaurant en weekdag veranderen zelden. Die worden 10 minuten bewaard zodat ze niet bij elke klik opnieuw worden opgehaald.

Stap 2 — haal variabele data in één keer op

Capaciteit per tijdslot wordt niet meer één voor één opgevraagd, maar in één gerichte vraag aan de database.

Stap 3 — bewaar beschikbaarheid heel kort

Beschikbare tijdsloten worden 30 seconden bewaard: snel bij herhaald klikken, maar nooit te verouderd om betrouwbaar te zijn.

Queryflow vergelijking

N+1 versus gecachede batchaanpak

Vóór: N+1 patroon (per tijdslot)

→ SELECT shifts WHERE date = ? (×1)

→ GET capacity tijdslot #1 (×1)

→ GET capacity tijdslot #2 (×1)

→ GET capacity tijdslot #3 (×1)

= N queries per interactie

Na: cache + batch (per request)

→ GET shifts (cache hit ✓) 0 queries

→ SELECT capacity WHERE id IN (1,2,3)

= 1 query per interactie

Cache TTL-strategie

Shifts (stabiel) → TTL 10 min

Openingsstatus → TTL 5 min

Beschikbare sloten → TTL 1 min

Trade-off

Caching is nuttig, maar alleen als je de vervaldatum bewust kiest

Pro

Minder databasehits, sneller antwoord voor herhaalde interacties en een eenvoudige implementatie zonder extra infrastructuur.

Con

Je moet bewust omgaan met verouderde data en expliciet nadenken over invalidatie zodra reservaties worden aangemaakt of gewijzigd.

Architectuurkeuze

Eerst query-reductie, dan pas zwaardere infrastructuur

Mijn mening is duidelijk: voor publieke widgets wil je eerst het aantal queries reduceren en slim cachen, voordat je naar complexere infrastructuur grijpt. Dat levert meestal sneller resultaat op en houdt de oplossing eenvoudiger te debuggen.

Waarom dit werkt

Eén batchquery is voorspelbaarder dan meerdere kleine queries. Een korte cache op stabiele of semi-stabiele data levert meteen winst zonder dat je architectuur onnodig zwaar wordt.

Resultaat

Consistentere widgetrespons met minder onnodig werk

Shifts worden niet meer bij elke interactie opnieuw uit de database gehaald.

Capaciteit wordt niet langer per tijdslot apart opgehaald, maar in één batch geladen.

De widget reageert consistenter bij herhaald klikken op datum, aantal personen en tijdslot.

De winst kwam uit query-reductie en gerichte caching, niet uit zwaardere infrastructuur.

Lessons learned

Cache bewust, niet reflexmatig

Cache alleen data die stabiel genoeg is om kort verouderd te mogen zijn.

Gebruik batch loading voor lijsten die anders per item een query triggeren.

Kies een korte TTL voor data die snel kan wijzigen, zoals beschikbare tijdsloten.

Denk vooraf na over expliciete invalidatie na reserveren of wijzigen.

Test widgets met meerdere opeenvolgende requests, niet alleen met één losse call.

Takeaway

N+1-problemen zijn zelden zichtbaar in de happy flow, maar worden snel een bottleneck onder herhaald gebruik

Cache stabiele data, batch-load variabele data en hou invalidatie bewust en klein. Dat geeft meestal sneller resultaat dan meteen infrastructuur verzwaren, en het houdt je widget veel beter beheersbaar.

Voor de technisch geïnteresseerde

Implementatiedetails

De onderstaande codevoorbeelden tonen hoe caching en batch loading concreet zijn uitgewerkt in C# met IMemoryCache.

1. Shifts cachen per restaurant en weekdag

string shiftsCacheKey = $"shifts:{restaurantId}:{weekdag}";
if (!_memoryCache.TryGetValue(shiftsCacheKey, out List<Shift> shiftsForDate))
{
    shiftsForDate = await _context.Shifts
        .Where(s => s.RestaurantId == restaurantId
                 && s.Weekdag == weekdag
                 && s.IsActief)
        .Include(s => s.ShiftTijdsloten)
        .ToListAsync();

    _memoryCache.Set(shiftsCacheKey, shiftsForDate, TimeSpan.FromMinutes(10));
}

2. Capaciteit batch-loaden in één query

var tijdslotIds = tijdsloten.Select(ts => ts.Id).ToList();
var capaciteiten = await _context.ReservatieCapaciteit
    .Where(rc => tijdslotIds.Contains(rc.ShiftTijdslotId) && rc.Datum.Date == datum.Date)
    .ToDictionaryAsync(rc => rc.ShiftTijdslotId);

3. Korte cache voor beschikbare tijdsloten

string cacheKey = $"tijdsloten:{restaurantId}:{datum:yyyy-MM-dd}:{aantalPersonen}";
if (_memoryCache.TryGetValue(cacheKey, out List<object> cachedResults))
{
    return Ok(cachedResults);
}

Veelgestelde vragen

Waarom niet meteen zwaardere infrastructuur gebruiken?
Omdat de grootste winst hier eerst zat in het wegnemen van onnodige databasecalls. Query-reductie en caching geven vaak sneller resultaat met minder complexiteit dan meteen extra infrastructuur toevoegen.
Waarom is een TTL van 10 minuten aanvaardbaar voor shifts?
Omdat shifts configuratie-data zijn die normaal niet constant verandert. Voor dat soort gegevens is een korte periode van lichte veroudering acceptabel als je daarmee veel reads uitspaart.
Waarom is de cache voor tijdsloten maar 30 seconden?
Omdat beschikbare tijdsloten sneller kunnen wijzigen door nieuwe reservaties. Daar wil je wel winst halen op herhaald klikken, maar zonder te lang vast te zitten aan oude waarden.
Wat is hier het echte risico als je te agressief cached?
Dat je beschikbaarheid of capaciteit toont die net intussen gewijzigd is. Daarom moet de cacheduur kort blijven voor volatiele data en moet invalidatie bewust voorzien worden bij reserveren of aanpassen.
Mitch

Merk je dat je widget of portaal functioneel klopt, maar onder herhaald gebruik onnodig zwaar blijft werken?

Plan een vrijblijvende digitale kennismaking met Mitch en ontdek wat wij voor jouw organisatie kunnen betekenen.