Blog #24: Failed Optimistic Update – When efforts to 'smooth' UI turn into UX disasters
Analyzing a technical mistake when applying Optimistic Update excessively to a social network feature and the lesson on interface honesty.
Team size: 8 people. Deadline: 2 weeks for the "Real-time Interaction" feature of an internal social network with 20,000 users. At that time, I was a Technical Lead and was extremely passionate about the Optimistic UI concept—where we show the user the result immediately before the server even responds.
The Problem: The beautiful "lie" of the interface
We were building the Like and Comment feature. I wanted users to feel the app was incredibly fast. When they click Like, the heart turns red immediately, and the number increases instantly.
But problems arose when the Backend system had an issue or the user's network was unstable. The request sent out failed (500 Error). At this point, we had to perform a "Rollback"—meaning turning it back from red to gray, and the increasing number had to decrease.
The user saw a "dancing" heart: Red -> Gray -> Red -> Gray. They felt confused and lost trust in the system: "Wait, did I actually Like successfully?".
Options Considered
We were caught between two choices:
Option 1: Use Standard Loading (The Boring Way)
- Solution: Click Like -> Show a small loading icon or blur the button -> Wait for Server OK -> Show red.
- Pros: Extremely honest. No "dancing" data. Simple code, easy to handle errors.
- Cons: The app feels "laggy" (delay). For small actions like Like, users usually don't want to see a loader.
Option 2: Optimistic Update with Queue & Retry (The High-End Way)
- Solution: Still show red immediately. But if an error happens, don't rollback immediately—put it into a Queue to retry in the Background. Only when 3 retries still fail do we notify the user.
- Pros: Super smooth app. Almost completely eliminates the feeling of waiting.
- Cons: Extremely complex to manage synchronization consistency. If the user clicks Like and Unlike continuously while the network is failing, the queue logic will become a mess.
Final Decision and Analysis
At that time, I was over-confident and chose Option 2.
// Pseudo-code of the mess I created
const onLike = (id) => {
// STEP 1: Update UI immediately (Optimistic)
updateUILocal(id, true);
// STEP 2: Put into action queue
actionQueue.push({ id, type: 'LIKE', timestamp: Date.now() });
// STEP 3: Handle in background with Retry strategy
processQueue(id).catch(err => {
// If completely down, show a red error notification
showGlobalError("Connection lost, please try again later.");
rollbackUI(id);
});
};
Impact on Performance: Didn't make the app slow technically, but consumed significant client-side resources for logic processing.
Impact on Maintainability: This was a disaster. Future developers didn't dare touch the interaction-manager.js file because the retry and rollback logic was so tangled. A small error in this layer crashed the whole user experience.
Impact on Team: Juniors were completely "lost" when debugging issues related to data synchronization. They didn't know why the state in the browser was different from the state in the DB.
Self-Reflection: The price of Over-engineering
Looking back, I realize I was seriously Over-engineering. For a Like feature, waiting for 300-500ms isn't a major issue. I turned a simple feature into a complex architectural problem just for a programmer's ego to show off "high-end" techniques.
If I went back to that time, I would choose Option 1 (Standard Loading) or a simpler Hybrid option: Only Optimistic for Like, while Comment (which needs higher precision) would use a Loader.
Lesson Learned
Never "lie" to your users if you aren't sure you can keep that promise until the end. Honesty and stability are sometimes much more valuable than the flashiness of speed.
Notes on a time I "fell off the horse" due to being too obsessed with UI smoothness.
Series • Part 24 of 50