Developing a Like System in a Mobile App
A like seems trivial: tap — increment counter — icon fills. But without optimistic updates, the button lags for 300–500 ms waiting for a server response, which subjectively kills the app feel. Double taps or rapid repeated clicks without debounce generate extra requests and can break the counter. In a real project, we encountered a situation where fast tapping increased the counter by 2 due to missing locking — adding debounce and atomic increment solved the issue. We design such systems turnkey, considering all edge cases: race conditions, offline mode, push notifications to the author, and eye-pleasing animations. Our experience: over 5 years in mobile development and dozens of commercial projects with likes, comments, and other reactions.
Why a Simple Like Implementation May Break the App?
Without optimistic updates, each tap introduces visible delay — the user thinks the app is slow. And if the button isn't protected against spam, a fast double tap sends two requests, potentially leading to an incorrect counter. Another issue is race conditions with parallel requests, where two increments cancel each other. On the server, use atomic UPDATE; on the client, debounce and blocking of repeated calls.
How Optimistic Updates Work
The standard in social apps is to update the UI instantly, without waiting for the server response. Optimistic updates are 2× faster for the user: subjective delay drops from 400 ms to 0.
iOS (UIKit):
func toggleLike(for post: Post) { let wasLiked = post.isLiked // Instantly change UI post.isLiked = !wasLiked post.likesCount += wasLiked ? -1 : 1 updateCell(for: post) // Server request apiService.toggleLike(postId: post.id) { [weak self] result in if case .failure = result { // Rollback post.isLiked = wasLiked post.likesCount += wasLiked ? 1 : -1 self?.updateCell(for: post) } } } On Compose similarly: likedState in ViewModel changes immediately, the request runs in parallel, on error the StateFlow reverts to the previous value.
How to Protect the Button from Spam and Duplicates?
Rapid double taps must be protected. The simplest way is an isRequesting: Bool flag at the ViewModel level, blocking repeated calls until a response is received. For more complex cases — debounce for 300 ms: send the final state (liked/unliked), not each tap. Using debounce reduces extraneous requests by 80%.
On Android with Kotlin Flow:
likeButtonClicks .debounce(300) .distinctUntilChanged() .flatMapLatest { liked -> toggleLikeUseCase(postId, liked) } .launchIn(viewModelScope) Animation and Counter
Animation of the like is a small detail that users notice. The Instagram approach: the heart "springs" on tap. On iOS — UIView.animate(withDuration: 0.1, animations: { button.transform = CGAffineTransform(scaleX: 1.3, y: 1.3) }) { _ in UIView.animate(...) { button.transform = .identity } }. On Compose — animateFloatAsState with spring(dampingRatio = 0.4f).
The color of the filled like via tintColor (iOS) or ColorFilter.tint (Compose). Icon — SF Symbol heart/heart.fill on iOS, Material Icon on Android.
Storing likes_count as a denormalized field in the post table is correct. Do not count SELECT COUNT(*) on every feed request. Increment/decrement via atomic UPDATE posts SET likes_count = likes_count + 1 WHERE id = ? — no race conditions. Like uniqueness: table likes (user_id, post_id, PRIMARY KEY (user_id, post_id)). Duplicates are impossible at the DB level.
Comparison of Approaches to Like Implementation
| Parameter | Simple Implementation | Optimistic Implementation |
|---|---|---|
| UI speed | Delay 300–500 ms | Instant |
| Spam protection | No | Debounce + isRequesting |
| Race condition | Possible | No (atomic SQL) |
| Offline support | No | Local storage + sync |
| Animation | Optional | Spring, customizable |
Work Process
- Analysis of current architecture and API — identify bottlenecks, estimate load.
- Data schema design — PostgreSQL/MySQL with atomic increments, Redis for hot counter.
- Client logic implementation — iOS (SwiftUI/UIKit), Android (Compose), Flutter. Embed debounce, blocking, animation.
- Push notifications integration — APNs/FCM, send on like with a 1-2 second delay for grouping.
- Testing — unit tests for ViewModel, UI tests for button behavior, load testing with 1000 requests per minute.
- Deployment — publish to App Store and Google Play, monitor crash-free rate.
Estimated Timelines
Basic implementation with optimistic updates, animation, and duplicate protection takes from 4 hours to 2 days per platform. Pricing is individual based on complexity and integrations (push, offline, cross-platform).
Typical Mistakes in Like Implementation
- Missing atomic increment on the server — the counter may lose likes under concurrent requests.
- Ignoring offline mode — user sees correct UI but data is lost on restart.
- Using
SELECT COUNT(*)in feed — slows query by 30-50%.
Order an audit of your current implementation, and we'll show which bottlenecks can be eliminated. Contact us for a consultation — we'll help implement a like system that doesn't lag and delights users. We guarantee stability and deadline adherence.
Full iOS Controller Example
```swift class LikeButton: UIButton { var isLiked: Bool = false { didSet { updateAppearance() } } private func updateAppearance() { let imageName = isLiked ? "heart.fill" : "heart" setImage(UIImage(systemName: imageName), for: .normal) tintColor = isLiked ? .red : .gray } } ```The optimistic update approach is described in Apple's official documentation for UIView.animate. For Compose, a similar pattern is described in the official guide.







