LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #24: Failed Optimistic Update – When efforts to 'smooth' UI turn into UX disasters
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

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.

TP
Truong PhamSoftware Engineer
PublishedMay 25, 2024

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

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #25: Context API Perf Hit – When the 'standard React' solution betrays you
Blog #23: Multi-tab Syncing – When your application 'talks' between tabs
19Blog #19: Chrome Performance Tab – Looking through a microscope at the system's pulse20Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?21Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument22Blog #22: Click Race Condition – When User's click speed beats your App23Blog #23: Multi-tab Syncing – When your application 'talks' between tabs24Blog #24: Failed Optimistic Update – When efforts to 'smooth' UI turn into UX disastersReading25Blog #25: Context API Perf Hit – When the 'standard React' solution betrays you26Blog #26: Don't let 'Single Source of Truth' become blind dogma27Blog #27: useEffect is not the place for doing all synchronizing logic28Blog #28: The naivety of believing setState is an immediate assignment29Blog #29: When 'Controlled Component' turns into a performance burden
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles