N
Noah Sheldon
BlogProjectsSkillsExperienceLinks
free ai audit
Home/Blog/Low Level Design/Product Design for Developers: Creating User-Centric Software
TutorialLow Level DesignDesignUX

Product Design for Developers: Creating User-Centric Software

Why technical excellence is not enough and how developers can build products that users actually love to use.

Jul 19, 2026
11 min

For years, I built software the wrong way. I started with the database schema, then the API, then the frontend. I spent weeks implementing features that nobody used. I optimised database queries while users struggled to find the login button.

It was not until building xThreads.app and PostQueue.app - products that needed actual users, not just working code - that I understood the real skill gap. Technical excellence alone does not create successful software. The most elegant architecture is worthless if the user cannot accomplish what they need.

This post is for developers who want to build products, not just features. It covers practical frameworks for user-centric design, decision-making heuristics for tradeoff situations, and concrete patterns you can apply to your next project - all based on lessons from shipping real SaaS products to real users.

What you will learn: Jobs to Be Done framework · Outcome-driven feature prioritisation · User feedback loops that actually work · Design principles for non-designers · Technical UX (performance, error handling, onboarding) · Measuring what matters - metrics that predict user retention


1. The Developer-Designer Divide

Developers and designers think differently, and that is by design. Developers optimise for correctness (does it work? is it efficient? is it maintainable?). Designers optimise for usability (can the user do what they need? is it intuitive? is it delightful?).

The best software comes from people who can bridge this gap - developers who ask "why does the user need this?" as often as they ask "how do I build this?"

Diagram 1: The developer-designer divide - where great products live

Developer Focus✓ Correctness✓ Performance✓ Maintainability✓ ScalabilityDesigner Focus✓ Usability✓ Accessibility✓ Visual hierarchy✓ User journeyGreat Products

The products that succeed are the ones that live in that overlap. They are technically sound and delightful to use. Let us look at how to get there.


2. Understand the Job, Not the User

"Know your user" is vague advice. A more actionable framework: Jobs to Be Done (JTBD). Instead of asking "who is our user?" (demographics, job titles, age), ask "what job are they hiring our product to do?"

For PostQueue.app, the job was not "schedule social media posts." The job was: "Maintain a consistent social media presence while focusing on my core business."That reframing changed everything - we stopped optimising for "more scheduling features" and started optimising for "less time spent on social media."

// Bad: Features that serve "schedule posts" (the functional job)
const BAD_FEATURES = [
    "Bulk CSV import for scheduling",
    "Recurring post templates",
    "Cross-platform analytics dashboard",
    "A/B testing for post variants",
];

// Good: Features that serve the real job ("maintain presence while working")
const GOOD_FEATURES = [
    "One-click content adaptation for all platforms",
    "AI-generated captions from a single draft",
    "Auto-schedule at optimal times - no manual choices",
    "Weekly summary: 'Here is what was posted this week.'",
];

Diagram 2: Jobs to Be Done - functional vs. emotional jobs

Functional Job"Schedule social posts"Real Need (JTBD)"Maintain presence while focusing on my business"Emotional: "Look professional"Emotional: "Don't miss opportunities"Emotional: "Stay relevant"

When you understand the real job, you stop building features. You start buildingoutcomes.


3. Outcomes, Not Features: A Practical Framework

Here is a concrete process I use to decide what to build. It shifts the question from "should we build X feature?" to "what outcome are we trying to achieve?"

Step 1: Define the Outcome

// Before (feature-focused):
const FEATURE_REQUEST = "Add a calendar view";

// After (outcome-focused):
const OUTCOME = "Users want to see their posting schedule at a glance " +
                "so they can plan their content week ahead of time.";

Step 2: Generate Solutions (Not Just One)

For every outcome, generate at least three possible solutions. The first idea is usually the most obvious and the least innovative:

interface Solution {
    name: string;
    effort: "low" | "medium" | "high";
    impact: number;  // 1-10
    confidence: number;  // 1-10
}

function scoreSolution(s: Solution): number {
    // Impact / Effort ratio, weighted by confidence
    const effortWeight = { low: 1, medium: 2, high: 4 };
    const score = (s.impact * s.confidence) / effortWeight[s.effort];
    return Math.round(score * 10) / 10;
}

// Outcome: "See schedule at a glance for weekly planning"
const solutions = [
    {
        name: "Full calendar view with drag-and-drop",
        effort: "high" as const,
        impact: 7,
        confidence: 6,
    },
    {
        name: "Weekly email summary of scheduled posts",
        effort: "low" as const,
        impact: 6,
        confidence: 9,
    },
    {
        name: "Simple list view grouped by day",
        effort: "medium" as const,
        impact: 9,
        confidence: 8,

    },
];

solutions
    .map((s) => ({ ...s, score: scoreSolution(s) }))
    .sort((a, b) => b.score - a.score)
    .forEach((s) => console.log(`${s.name}: ${s.score}`));
// "Simple list view grouped by day: 36"
// "Weekly email summary: 54"
// "Full calendar view: 10.5"

Diagram 3: Impact vs. Effort - which solution to build first

High impact, Low effort← DO THESE FIRSTHigh impact, High effortLow impact, Low effortLow impact, High effort← NEVER DO THESE1Email summary2Simple list view3Calendar view (deferred)

The email summary scored highest because it is low-effort and highly impactful - it lands in the user's inbox without them having to log in. The calendar view is a feature that sounds good but costs 4x as much to build for similar impact.


4. User Feedback Loops That Actually Work

"Talk to users" is another piece of advice that sounds obvious but is rarely done well. Here is the feedback loop I use for every product I build:

interface FeedbackLoop {
    // Quantitative: what users DO
    inAppTracking: {
        dailyActiveUsers: number;
        featureAdoptionRate: Record<string, number>;
        sessionDuration: number;    // seconds
        completionRate: number;    // % of users who finish key flow
        churnRate: number;          // % who stop using after 7 days
    };

    // Qualitative: what users SAY
    userResearch: {
        interviewCadence: "weekly";   // 3 users/week during MVP
        npsScore: number;
        topComplaints: string[];
        topRequests: string[];
    };
}

// ── How to collect each metric ──

// 1. Feature adoption (code-driven)
async function trackFeatureUsage(userId: string, feature: string) {
    await db.query(
        "INSERT INTO feature_events (user_id, feature, timestamp) " +
        "VALUES ($1, $2, NOW())",
        [userId, feature]
    );
}

// 2. Session duration (simple version)
const SESSION_TIMEOUT = 30 * 60 * 1000;  // 30 minutes

async function trackSession(userId: string) {
    const lastSession = await db.query(
        "SELECT started_at FROM sessions " +
        "WHERE user_id = $1 AND ended_at IS NULL",
        [userId]
    );

    if (lastSession.length > 0) {
        const elapsed = Date.now() - lastSession[0].started_at;
        if (elapsed > SESSION_TIMEOUT) {
            // End previous session
            await db.query(
                "UPDATE sessions SET ended_at = NOW() " +
                "WHERE user_id = $1 AND ended_at IS NULL",
                [userId]
            );
        }
    }
}

// 3. In-app feedback prompt (triggered after 5th session)
async function maybePromptForFeedback(userId: string) {
    const { sessionCount } = await db.query(
        "SELECT COUNT(*) as count FROM sessions WHERE user_id = $1",
        [userId]
    );

    if (sessionCount === 5) {
        // Show: "How likely are you to recommend this to a friend?"
        // Store NPS response
    }
}

The key insight: never ask users what they want. Ask them what they are trying to do. A user who says "add a dark mode" might actually need "reduce eye strain during late-night editing" - and the right solution might be a font size toggle, not a dark theme.


5. Design Principles for Non-Designers

You do not need to be a designer to build good-looking, usable software. These four principles cover 90% of what matters:

Principle 1: Consistency

Every button of the same type should look the same. Every error message should follow the same structure. Every loading state should use the same pattern. Consistency is the single highest-leverage design improvement for developers because it costs nothing and improves everything:

// Bad: Inconsistent patterns everywhere
const ERROR_STYLES = [
    { show: () => alert(error) },           // browser alert
    { show: () => toast(error) },           // toast notification
    { show: () => setErrorState(error) },   // inline error
    { show: () => console.error(error) },   // silent (the worst)
];

// Good: One consistent error pattern
function showError(message: string, type: "toast" | "inline" = "toast") {
    if (type === "toast") {
        toast({ title: "Error", description: message, variant: "destructive" });
    } else {
        setErrorState(message);
    }
}

// Every error in the app goes through showError()
try {
    await api.createPost(data);
} catch (err) {
    showError("Failed to create post. Please try again.");
}

Principle 2: Simplicity

Every UI element competes for the user's attention. The easiest way to improve your UI is to remove things. For every element on the page, ask: "Does this help the user accomplish their goal right now?"

Diagram 4: Attention competition - every element reduces focus on the others

User's Attention BudgetPrimary actionNavigationSidebarAdsFooterRemove every element that does not help the user's goal

Principle 3: Feedback

Every user action should have a visible response within 100ms (perceived instant), and a result within 1 second (perceived immediate). Between 1-10 seconds, show progress. Over 10 seconds, show an estimated time:

// Feedback patterns by duration
function handleAction(action: () => Promise<void>) {
    // 0-100ms: Instant visual feedback
    setButtonState("loading");

    // 100ms-1s: Spinner is enough
    setTimeout(async () => {
        try {
            await action();
        } catch (err) {
            // 1-10s: Show error with retry option
            showError("Something went wrong", {
                action: "Retry",
                onAction: () => handleAction(action),
            });
        }
    }, 100);
}

// For operations > 10 seconds: Show progress
async function uploadLargeFile(file: File) {
    const startTime = Date.now();
    setProgress("Preparing upload...");

    for (const chunk of chunkFile(file)) {
        await uploadChunk(chunk, (progress) => {
            const elapsed = (Date.now() - startTime) / 1000;
            setProgress(
                `Uploading... ${Math.round(progress)}% (${elapsed.toFixed(0)}s)`
            );
        });
    }

    setProgress("Processing...");
}

Principle 4: Forgiveness

Users make mistakes. Good design anticipates them and provides escape routes:

  • Undo: Every destructive action should be reversible or have an undo window. Gmail's "Undo Send" is the gold standard.
  • Confirmation: Destructive actions (delete, cancel subscription) need a confirmation dialog with the consequences stated clearly.
  • Auto-save: Never lose user work. Save drafts automatically every 30 seconds.
// Auto-save pattern
function useAutoSave(data: Record<string, unknown>, intervalMs = 30_000) {
    const lastSaved = useRef<string>("");

    useEffect(() => {
        const timer = setInterval(async () => {
            const serialized = JSON.stringify(data);
            if (serialized === lastSaved.current) return; // no changes

            try {
                await api.saveDraft(data);
                lastSaved.current = serialized;
                showToast("Draft saved", { variant: "success" });
            } catch {
                // Silent fail - auto-save retries on next interval
                console.warn("Auto-save failed, will retry");
            }
        }, intervalMs);

        return () => clearInterval(timer);
    }, [data, intervalMs]);
}

6. Technical UX: Speed Is a Feature

Users equate speed with quality. A slow app feels broken, regardless of how well it is built. Here are the technical UX patterns that have the biggest impact on user perception:

Progressive Loading

Never show a blank page while data loads. Show the page shell immediately and fill in content as it arrives:

// Loading state as the initial state (not null)
function PostDashboard() {
    return (
        <div>
            <h1>Your Posts</h1>
            <PostList />  // Always renders - shows skeleton initially
        </div>
    );
}

function PostList() {
    const { data, isLoading } = usePosts();

    if (isLoading) {
        return (
            <div className="space-y-3">
                {[1, 2, 3].map((i) => (
                    <div key={i} className="skeleton h-20 rounded-lg" />
                ))}
            </div>
        );
    }

    return data.map((post) => <PostCard key={post.id} post={post} />);
}

Optimistic Updates

When the user performs an action, update the UI immediately - do not wait for the server response. If the server fails, roll back:

// Optimistic update with rollback
async function toggleLike(postId: string) {
    // 1. Update UI immediately
    const previousLikes = likes;
    setLikes(likes + 1);
    setLiked(true);

    try {
        // 2. Send API request in background
        await api.likePost(postId);
    } catch {
        // 3. Rollback on failure - no interruption to the user
        setLikes(previousLikes);
        setLiked(false);
        showError("Failed to like post", { duration: 3000 });
    }
}

Diagram 5: Optimistic updates - the timeline comparison

Without optimistic updateClick → Spinner → Wait 500ms → DoneWith optimistic updateClick → Instant ✓ → API in backgroundPerceived latency: 500ms → 0msUsers perceive the app as instantaneous

7. Measuring What Matters

Vanity metrics (page views, registered users) tell you nothing about product quality. Here are the metrics that predict whether users will stick around:

  • Activation rate: % of signups who complete the core action within the first session. For xThreads: generated their first reply. For PostQueue: scheduled their first post. This is the strongest predictor of 30-day retention.
  • Time-to-value: How long does it take from signup to the first "aha" moment? Under 2 minutes is ideal. Over 5 minutes, you lose half your users.
  • Feature stickiness: % of active users who use a feature daily vs. weekly vs. once. If a feature is not sticky, either improve it or remove it.
  • Completion rate: For any multi-step flow (onboarding, scheduling, payment), what % of users complete every step? Each drop-off point is a fix opportunity.
// Activation tracking - the single most important metric
async function trackActivation(userId: string, activationEvent: string) {
    "Track whether a user has reached their 'aha' moment."
    const existing = await db.query(
        "SELECT id FROM activation_events " +
        "WHERE user_id = $1",
        [userId]
    );

    if (existing.length === 0) {
        // First activation - this is the moment that predicts retention
        await db.query(
            "INSERT INTO activation_events (user_id, event, timestamp) " +
            "VALUES ($1, $2, NOW())",
            [userId, activationEvent]
        );

        // Track time-to-value
        const { signupTime } = await db.query(
            "SELECT created_at FROM users WHERE id = $1",
            [userId]
        );
        const ttv = Date.now() - new Date(signupTime).getTime();
        console.log(`Time-to-value: ${Math.round(ttv / 1000)}s`);
    }
}

// Use it:
// In xThreads: trackActivation(user.id, "generated_first_reply")
// In PostQueue: trackActivation(user.id, "scheduled_first_post")

8. Summary: From Developer to Product Builder

Understand → Prioritise → Build → Measure → Iterate
  • Great products live at the intersection of technical excellence and user-centric design. You need both to succeed.
  • Use Jobs to Be Done to understand what users are really hiring your product for - then focus every feature on that job.
  • Prioritise features by outcome, not by request. Three solutions per outcome. Score by impact/effort × confidence.
  • Consistency, simplicity, feedback, and forgiveness cover 90% of good design. Apply them systematically.
  • Technical UX - progressive loading, optimistic updates, skeleton screens - makes your app feel fast and reliable.
  • Track activation rate and time-to-value above all else. These predict retention better than any other metric.

Your next challenge

# TODO: Audit your current project
#
# 1. Pick one core user flow in your product
# 2. Map out every step the user takes
# 3. Identify: where do users drop off? Where do they get confused?
# 4. For each drop-off, ask: "What job is the user trying to do here?"
# 5. Generate 3 solutions - pick the one with best impact/effort ratio
# 6. Implement it, measure the change, iterate

# User flow audit template:
USER_FLOW = {
    "name": "Complete purchase",
    "steps": [
        {"name": "Select plan", "drop_off": "12%", "issue": "Too many options"},
        {"name": "Enter payment", "drop_off": "30%", "issue": "No Apple Pay"},
        {"name": "Confirm", "drop_off": "5%", "issue": ""},
        {"name": "Done", "drop_off": "0%", "issue": ""},
    ],
    "activation_event": "first_payment",
}

The best developers I know are the ones who care about users. They do not see product thinking as a separate skill - they see it as part of engineering. Because what is engineering, really, if not building things that work for people?

Chapter

Low Level Design

Reading level

Tutorial

About the author

NS

Noah Sheldon

Applied AI/ML @ Fitch

NS

Noah Sheldon

Associate Director, Applied AI/ML @ Fitch Ratings

Get a free AI opportunity audit

never miss a deep dive

New essays land when they ship. No spam, unsubscribe anytime.