Recipes
Daily Usage Quota
Count per user per day; the key cleans itself up:
typescript
import { Injectable, Inject } from '@nestjs/common';
import { COUNTER_SERVICE, ICounterService } from '@nestjs-redisx/counters';
@Injectable()
export class UsageService {
constructor(
@Inject(COUNTER_SERVICE)
private readonly counters: ICounterService,
) {}
// Window counter: the TTL applies only when the increment CREATES the
// key - later increments never extend the window, so daily keys expire
// on their own without a cleanup job.
async onApiCall(userId: string): Promise<number> {
const today = new Date().toISOString().slice(0, 10);
return this.counters.increment(`api:${userId}:${today}`, 1, { ttl: 86_400_000 });
}
// Flush-style consumption: read the accumulated total and zero it in ONE
// atomic step - periodic aggregation without double counting.
async flushEmailBatch(): Promise<number> {
return this.counters.getAndReset('emails:batch');
}
}Quota check at the call site:
typescript
const used = await this.counters.increment(`api:${userId}:${today}`, 1, { ttl: 86_400_000 });
if (used > plan.dailyLimit) {
throw new PaymentRequiredException(); // the increment still counted the attempt
}For token-bucket / sliding-window enforcement with guards and headers, use the rate limiter - quotas here are domain accounting.
Batch Flush Aggregation
Accumulate cheaply on the hot path, ship totals periodically, never double-count:
typescript
@Cron(CronExpression.EVERY_MINUTE)
@LeaderOnly() // one instance ships the batch
async shipEmailStats() {
const sent = await this.counters.getAndReset('emails:batch');
if (sent > 0) await this.analytics.track('emails_sent', sent);
}Live Statistics
typescript
import { Injectable, Inject } from '@nestjs/common';
import { COUNTER_SERVICE, ICounterService } from '@nestjs-redisx/counters';
@Injectable()
export class StatsService {
constructor(
@Inject(COUNTER_SERVICE)
private readonly counters: ICounterService,
) {}
// Atomic fleet-wide increment - concurrent updates never get lost.
async onArticleView(articleId: string): Promise<number> {
return this.counters.increment(`views:${articleId}`);
}
async getViews(articleId: string): Promise<number> {
return this.counters.get(`views:${articleId}`); // 0 when absent
}
// Counters go negative freely - clamp in domain code if needed.
async releaseSeat(eventId: string): Promise<number> {
return this.counters.decrement(`seats:${eventId}`);
}
async resetViews(articleId: string): Promise<boolean> {
return this.counters.reset(`views:${articleId}`);
}
}Seat / Stock Inventory
typescript
const left = await this.counters.decrement(`seats:${eventId}`);
if (left < 0) {
await this.counters.increment(`seats:${eventId}`); // compensate
throw new ConflictException('sold out');
}For bounding concurrent work rather than stock, use the semaphore.
Fleet-Wide ID Generation
typescript
import { Injectable, Inject } from '@nestjs/common';
import { COUNTER_SERVICE, ICounterService } from '@nestjs-redisx/counters';
@Injectable()
export class InvoiceNumberService {
constructor(
@Inject(COUNTER_SERVICE)
private readonly counters: ICounterService,
) {}
// Monotonic fleet-wide sequence: 1, 2, 3, ... with no collisions across
// instances. Sequences have no TTL by definition.
async nextInvoiceNumber(): Promise<string> {
const n = await this.counters.next('seq:invoices');
return `INV-${String(n).padStart(6, '0')}`;
}
async lastIssued(): Promise<number> {
return this.counters.current('seq:invoices'); // 0 before the first next()
}
}