Back to the journal

Your code review comments say 'looks good' because the diff is 900 lines and nobody wants to be the one who delays the sprint

Large pull requests don't get reviewed. They get skimmed. Once a diff crosses about 300 lines, reviewers start pattern-matching instead of reading — scanning for obviously wrong things, checking that tests exist, and approving. The mental cost of genuinely understanding a 900-line change across six files is high enough that most people won't do it at 4pm on a Wednesday. So 'LGTM' becomes a social signal that means 'I trust you' rather than 'I verified this.'

The fix isn't stricter review guidelines or longer review windows. It's smaller pull requests. We enforce a soft cap of around 250 lines of meaningful diff (excluding generated files and lock files). That means features get broken into sequential PRs: data layer first, then business logic, then the UI surface. Each one is reviewable in ten minutes with actual focus. The side effect is that each PR also becomes easier to revert, easier to bisect when something breaks, and easier to onboard a new developer with — they can read the merge history as a narrative instead of a wall.

If your team's review turnaround is slow and your approval comments are thin, the problem probably isn't laziness. It's batch size. Measure your average diff size for the last month. If most PRs land above 400 lines, start splitting them at the authoring stage rather than asking reviewers to work harder. A review that actually catches something is worth ten rubber-stamped approvals.

You scoped your MVP to three months because that's how long the budget lasts, not because the product needs three months of work

Most MVPs we see aren't scoped by what the market needs to validate — they're scoped by how much runway the founder has left. Someone has R400k set aside, a dev shop quotes three months, and the feature list expands to fill exactly that container. The result is a product that does twelve things adequately instead of two things well enough to prove anyone will pay. The budget becomes the brief, and nobody stops to ask which of those twelve features actually tests the core bet.

The discipline is brutal but simple: write down the one sentence that describes why a customer would switch from whatever they're doing today. Then cut every feature that doesn't directly test whether that sentence is true. If your bet is that restaurant owners will pay for automated supplier re-ordering, you don't need a dashboard, a reporting module, or user roles in your first release. You need one restaurant owner placing one real order through your system and telling you whether it saved them time. Everything else is furniture in a house you haven't proven anyone wants to live in.

This matters especially when you're paying a studio or contractor by the month. Every feature you add to an MVP doesn't just cost dev time — it delays the moment you get real feedback, which is the only thing that actually de-risks your investment. Scope your MVP to the question, not the quarter. Build the smallest thing that forces a potential customer to say yes or no, then let their answer shape what month two looks like.

Your largest contentful paint is a hero image that nobody scrolls past anyway

Most marketing sites we audit have the same problem: a full-bleed hero image that weighs 400–800 KB, loads as a render-blocking resource, and tanks Largest Contentful Paint scores. The image is usually decorative — a stock photo of people shaking hands or a gradient with text overlaid — and analytics show that visitors scroll past it within two seconds. It is simultaneously the heaviest element on the page and the least useful one.

The instinct is to optimise the image: convert to WebP, add srcset, lazy-load it. Those steps help, but they miss the deeper question. If the hero image is not doing meaningful work — communicating a product benefit, showing a real screenshot, giving the visitor a reason to stay — then you are spending your entire LCP budget on decoration. Swapping it for a well-styled text block with a solid background colour can drop your LCP from 3.5 seconds to under 1 second with zero tooling changes. We did exactly this on a client's landing page last month and their mobile performance score jumped from 54 to 91.

Before you reach for an image CDN or start fiddling with priority hints, audit what your LCP element actually is and whether it earns its weight. Run Lighthouse, check which element gets flagged, and ask honestly: does this image make anyone more likely to convert? If the answer is no, remove it and let your headline do the work. A fast page with a clear value proposition will always outperform a slow page with a pretty picture.

Your Flutter app ships one binary but your push notification logic ships three completely different implementations

Flutter gives you a single codebase for iOS and Android, and that promise holds up remarkably well for UI, state management, and even most platform channel work. Then you wire up push notifications. Suddenly you're maintaining Firebase Cloud Messaging setup for Android, APNs certificates and entitlements for iOS, and a completely separate flow for web if you've got a PWA target. The platform-specific configuration files, the background handler registration, the permission request timing — none of it is truly cross-platform. You just happen to edit it from the same IDE.

The pain compounds when you factor in notification channels on Android (required since API 26), provisional authorisation on iOS, and the fact that each platform silently drops notifications for slightly different reasons. Android kills your background isolate if the OEM's battery optimiser doesn't like you. iOS won't deliver if the user dismissed your permission prompt once six months ago and you never built a soft-prompt gate. Testing this matrix manually is brutal, and most teams only discover the gaps when a client says 'I never got the alert.'

If you're building a Flutter app that depends on timely push delivery — think booking confirmations, load-shedding schedules, or payment alerts — treat notifications as a first-class subsystem, not a plugin you drop in during the last sprint. Write integration tests per platform. Build a soft-prompt screen that explains why notifications matter before the OS prompt fires. And test on at least two Android OEMs, because Samsung and Xiaomi treat background processes very differently from stock Android.

The estimate you gave over coffee is the one the client will hold you to forever

Every studio has done it. A client describes a feature in a casual conversation — maybe a WhatsApp voice note, maybe a braai — and you say something like 'that's probably two weeks.' You meant it as a rough gut feel. They heard a commitment. Six weeks later when it's still not done, they're not upset about the complexity you discovered along the way. They're upset because you said two weeks and it's been six, and no amount of explaining will undo that first number.

The problem isn't that verbal estimates are inherently wrong. It's that they travel without context. A written estimate in a proposal carries caveats, assumptions, and a scope boundary. A verbal number carries none of that. It gets relayed to a co-founder, dropped into a board deck, used to plan a launch date. By the time it reaches the person making decisions, your 'probably' has been completely stripped away. You've anchored a price and a timeline without meaning to, and now every subsequent conversation is measured against that anchor.

The fix is boringly simple: never give a number without giving it a home. If a client asks for a ballpark in conversation, say 'let me write that up by tomorrow.' It doesn't need to be a formal proposal — three bullet points in an email with assumptions listed will do. The act of writing forces you to think about what you're actually promising, and it gives the client something to forward that still has your caveats attached. Protect your estimates by putting them somewhere they can't be misquoted.

Your inventory lives in a Google Sheet that three people edit simultaneously and nobody trusts

We see this constantly with small wholesale and retail businesses in Cape Town: someone built a stock sheet in 2019, it worked fine when one person managed it, and now three staff members paste into it from different tabs while a fourth exports a CSV to email the accountant on Fridays. Nobody is sure which column is authoritative. Negative stock values appear and get manually corrected. The sheet has become the system of record for the entire operation, and it contradicts itself twice a week.

The instinct is to jump straight to a full inventory platform — something like Dear Systems or Cin7 — but the actual problem is usually narrower than it looks. Most of these businesses don't need real-time warehouse management. They need one trustworthy stock count that feeds into Xero or Sage without someone re-keying numbers. A lightweight integration layer, sometimes just a simple database with a clean front end and an API hook into the accounting package, removes the multi-editor chaos without importing a enterprise-grade system nobody will configure properly.

Before you automate anything, freeze the spreadsheet and get two people to independently count stock against it. If the numbers diverge by more than five percent, your data is already too dirty to migrate — clean it first or you are just piping garbage into a better-looking pipe. Then pick the smallest credible loop: stock count goes in once, syncs to your invoicing or accounting tool, done. Expand from that single reliable loop rather than wiring up six integrations on day one.

GPT-4o rewrites your error messages and now your support team can't find them in the codebase

One of the quickest wins people find with LLMs is rewriting user-facing copy — error messages, tooltips, confirmation dialogs. You paste in something like "Error 422: validation failed on field 'billing_address'" and GPT-4o hands back a friendly sentence a human would actually understand. The temptation is to do this across your entire app in an afternoon. We watched a client do exactly that: they fed about eighty error strings through a prompt, got polished results, committed them, and shipped. Two weeks later a customer reported a payment flow issue and the support engineer grep'd the codebase for the phrase the customer quoted. Nothing. The wording had been rewritten, the old string keys were gone, and nobody had updated the internal troubleshooting doc.

The problem isn't that the copy got better — it did. The problem is that generated copy has no institutional memory attached to it. When a developer originally wrote "ERR_BILLING_MISMATCH" they also wrote a Slack message explaining it, linked it in a runbook, maybe referenced it in a Sentry alert rule. The new friendly sentence exists in none of those places. Multiply that across dozens of strings and your observability layer quietly detaches from what users actually see.

If you're using an LLM to rewrite user-facing text, treat it like a migration, not a find-and-replace. Keep the original string keys or error codes as internal identifiers. Map the new copy back to your monitoring and support docs before you ship. The rewrite takes five minutes; the re-linking takes an hour. Skip the hour and your team loses the ability to trace a customer complaint back to a line of code.

FROM IDEAS TO SOMETHING REAL

Your next chapter.
Let’s build it.

A website, an app, a better way to work. Tell us what you have in mind.

Start a conversation