Without a review system, an app loses one of its main tools for social proof — users don't see others' experiences and don't leave their own. We've encountered projects where the aggregate rating is skewed by a few early reviews, photos take 4 seconds to load, and bots bypass moderation. We develop a turnkey review system starting from 3 business days — star ratings, text reviews, moderation, photos, and business replies. Get a consultation — we'll evaluate your project for free.
What usually breaks in custom implementations
Rating aggregation and updates
The most common mistake is to calculate the average rating on the fly with SELECT AVG(rating) across the entire reviews table on every product page request. At 50,000 reviews this starts to slow down. The correct approach: a denormalized average_rating and reviews_count field on the server side, updated via a trigger or a queue (Celery/Sidekiq/BullMQ) when a review is added/changed/deleted. The client receives the precomputed value.
On mobile, the rating should be displayed as a star indicator — iOS and Android implement this differently. In UIKit, we build a custom UIView with CALayer masks or assemble it from five UIImageView with .full, .half, .empty states. In Jetpack Compose — a Row with Icon and calculation via floor/ceil of the fractional value. Fill animation on first load is done via withAnimation (Compose) or UIView.animate changing clip-mask width.
Pagination and infinite scroll in review lists
Classic OFFSET/LIMIT works poorly with a large number of reviews — on the 10,000th page, the database still scans the entire index up to the required offset. We use cursor-based pagination: sort by created_at DESC, id DESC, return a next_cursor (base64 of the last id + timestamp), and the next request passes it as a parameter. Cursor-based pagination works 10 times faster than OFFSET/LIMIT on datasets of 10,000+ records.
On iOS, the list is built with UICollectionView and UICollectionViewDiffableDataSource — adding a new page via applySnapshot without flickering. prefetchDataSource requests the next page when 3-4 cells are left to the end. On Android — LazyColumn with LazyPagingItems from Paging 3.
Review photos
Uploading photos directly through the main API is an antipattern. The correct scheme: the client requests a presigned URL from an S3-compatible storage (AWS S3, Cloudflare R2, MinIO), uploads the file directly there, and then only passes the object key to the API. Compression before upload is done on the client: on iOS via UIImage.jpegData(compressionQuality: 0.75), on Android via Bitmap.compress(Bitmap.CompressFormat.JPEG, 75, outputStream). Limit: 2-3 photos, max 5 MB per file after compression.
Display uses Kingfisher (iOS) or Coil (Android) with a placeholder and 200ms crossfade. For a gallery on tap — modal UIPageViewController or HorizontalPager in Compose with pinch-to-zoom.
How the full implementation works
Data structure. A review contains: user_id, entity_id (product, service), entity_type, rating (1-5), body (text, optional), photos[], status (pending/approved/rejected), helpful_count, created_at. Indexes: (entity_id, entity_type, status, created_at DESC) for fetching approved reviews per entity.
Moderation. Automatic pre-filter via a profanity filter (the bad-words library or custom list on the backend) + a flag for manual review for reviews with keywords. Photos go through AWS Rekognition Moderation Labels or Google Cloud Vision SafeSearch before publication. In the moderator panel — a queue with approve/reject and the ability to reply to the review.
Reply to review. Business replies to a review as a separate entity review_reply (one-to-one with review). When a reply is published, a push notification is sent to the author via FCM/APNs with a deeplink to the review.
Helpful vote. helpful_votes — a separate table (user_id, review_id, UNIQUE). Limit: one vote per account. On the client — optimistic counter update with rollback on error.
Purchase verification. If the platform allows, we mark reviews from actual buyers with a "Verified Purchase" badge by checking for a closed order with the user_id and entity_id.
| Component | Technology | Specifics |
|---|---|---|
| Star rating | SwiftUI / Jetpack Compose | Denormalized field, trigger update |
| Pagination | cursor-based | Stable speed at 1M+ records |
| Moderation | bad-words + AWS Rekognition | Auto pre-filter + manual queue |
| Photos | presigned URL + S3/Coil/Kingfisher | Client-side compression to 5 MB |
| Business replies | review_reply, push notifications | FCM/APNs with deeplink |
| Voting | UNIQUE (user, review) | Optimistic update |
Why cursor-based pagination is better than OFFSET/LIMIT
Cursor-based pagination guarantees stable speed regardless of the number of pages. At 1,000,000 reviews, a cursor-based query executes in the same milliseconds as on the first page. PostgreSQL research shows that OFFSET on large datasets leads to a full index scan up to the offset. On mobile this is critical — the user should not wait for review loading longer than 200 ms.
How photo moderation is organized
Automatic checks via AWS Rekognition Moderation Labels or Google Cloud Vision SafeSearch detect NSFW content. If triggered, the review is flagged for manual review. The moderator in the panel sees the photo and text, and can approve or reject. This reduces the burden on the team and prevents unwanted content.
Work stages
- Audit of the current implementation (if any).
- Data schema and API design.
- Backend development.
- Mobile UI (both platforms or one).
- Moderation integration.
- Load testing (Artillery/k6 with scenario "500 concurrent reviews").
- Release.
For Flutter projects the entire UI is built once, with logic extracted into ReviewBloc (BLoC) or ReviewNotifier (Riverpod).
What's included
- Data schema design (ER diagram, index descriptions)
- REST/GraphQL API with documentation (Swagger/GraphQL Playground)
- Mobile UI for iOS/Android or Flutter
- Moderation integration (automatic + manual)
- Load testing and optimization
- Access to repository, CI/CD pipeline
- Training for the client's team (1 session)
- 2-week post-release support
Timeline
Basic system (star rating, text review, list with pagination, status-based moderation) — 3-5 business days. With photos, business replies, voting, and purchase verification — 8-12 days. Cost is calculated individually after requirements analysis.
Common implementation mistakes
- Calculating the rating on the fly without caching - Using OFFSET/LIMIT for pagination - Not compressing photos before upload - Skipping moderation of NSFW-content photos - Not enforcing unique votesContact us — we have implemented 20+ review systems for marketplaces and services over 5 years. We'll evaluate your project in 1 day. Get a consultation now.







