# Review counts and ratings updated near-real-time on submission

- **Pattern:** `ab-001034` (`directory-listing-schema.data-freshness.rating-freshness`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001034
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001034)

## Why it matters

Rating counts and averages that update on a nightly batch job rather than in real time allow the displayed schema-org `aggregateRating` to diverge from the database truth for hours after a review is submitted. A listing that receives 10 new 1-star reviews after a service failure continues displaying its pre-incident 4.5-star average until the batch runs — actively misleading users making decisions based on the structured data. Data-integrity is the core taxon: the JSON-LD `reviewCount` fed to Google must match the actual review count at page-render time. Delayed updates also suppress Google's trust in your rating freshness, reducing the likelihood of rich-result display.

## Severity rationale

High because batch-delayed rating updates allow structured data to misrepresent actual listing quality for hours, creating a data-integrity gap that Google's structured data guidelines prohibit.

## Remediation

Recalculate `averageRating` and `reviewCount` synchronously in the same transaction that writes the review. Avoid recomputing from a full table scan on every submit — maintain running aggregates on the listing row.

```ts
// src/app/api/listings/[id]/reviews/route.ts
export async function POST(request: Request, { params }: { params: { id: string } }) {
  const { rating, text } = await request.json();

  await db.$transaction(async (tx) => {
    await tx.review.create({ data: { listingId: params.id, rating, text } });

    // Recompute aggregate from all reviews in the same transaction
    const { _avg, _count } = await tx.review.aggregate({
      where: { listingId: params.id },
      _avg: { rating: true },
      _count: { rating: true },
    });

    await tx.listing.update({
      where: { id: params.id },
      data: {
        averageRating: parseFloat((_avg.rating ?? 0).toFixed(2)),
        reviewCount: _count.rating,
        dateModified: new Date(),
      },
    });
  });

  return Response.json({ ok: true });
}
```

The `dateModified` update in the same transaction ensures the JSON-LD timestamp stays current.

## Detection

- **ID:** `rating-freshness`
- **Severity:** `high`
- **What to look for:** Count all listings with ratings. For each, find the review submission API or logic. After a new review is submitted, check whether the listing's `averageRating` and `reviewCount` are immediately recalculated and persisted. Look for cache invalidation or real-time updates.
- **Pass criteria:** Review counts and ratings are recalculated within seconds of a new review being submitted (or approved) — ratings older than 6 months must be flagged or weighted differently in the aggregate score. Report even on pass: "X rated listings checked, Y have ratings older than 6 months."
- **Fail criteria:** Ratings are updated only on a schedule (e.g., hourly) or not updated until the next page refresh.
- **Skip (N/A) when:** Listings don't have reviews.
- **Detail on fail:** Example: `"Review submitted but reviewCount stays same for 1 hour until batch job runs"` or `"New review changes rating but listing page doesn't reflect it until next visit"`
- **Remediation:** Recalculate ratings immediately on review submission:

  ```ts
  export async function POST(request) {
    const { listingId, rating, text } = await request.json()

    const review = await db.review.create({
      data: { listingId, rating, text }
    })

    const reviews = await db.review.findMany({
      where: { listingId }
    })
    const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length

    const listing = await db.listing.update({
      where: { id: listingId },
      data: {
        averageRating: parseFloat(avgRating.toFixed(2)),
        reviewCount: reviews.length,
        dateModified: new Date()
      }
    })

    return NextResponse.json(listing)
  }
  ```

---

## External references

- schema-org AggregateRating
- schema-org reviewCount

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001034
