Stop Overengineering Your MVP — Build Something That Actually Runs | New Jersey Software

Custom Software Development in New Jersey

Stop Building the Architecture of a Billion-User App for 3 Test Users

Most ideas don't fail because they're bad. They fail because nothing ever actually runs. Somewhere between the idea and the implementation, things start to expand.

"We'll need a backend." "We should make it scalable." "Let's design this properly." "We might as well structure it like a real system."

Before long, you're not building a product anymore. You're designing infrastructure.

And nothing works yet.


I've seen this pattern over and over again. And to be honest, I've done it myself.

You start with something simple, and within a few hours you're thinking about services, layers, architecture, scaling ; what this will look like later. All before a single real output exists.

So for this project, I forced a constraint:

Build the smallest possible version that produces a real result.

Not a full system. Not something scalable. Not something "production ready."

Just something that works.


I built a minimal MVP for a prediction idea. No infrastructure. No backend. No deployment strategy.

Just a simple flow:

input → prediction → output

That's it. And that alone already puts it ahead of most ideas.

Because it runs.


The Subtle Drift

What's interesting is how quickly things try to expand, even when you're actively trying to keep them small.

The project started to look like this:

src/
  components/
  hooks/
  services/
  types/

And yeah… it looks good. Clean. Organized. Professional. It looks like "real engineering." But if we're being honest, even this might be more than the problem actually needs.

That's the issue you want to avoid.

You can build something that looks like a system long before it actually is one.


The first commit in this project wasn't about building something impressive. It was about proving something simple:

Given an input, can we produce a prediction?

That's it. No abstractions beyond what's necessary. No optimization. No scale. Just a working loop.


Why This Matters More Than Ever

Code generation is fast now. Anyone can produce code.

But most production problems don't come from writing code. They come from how systems behave over time—edge cases, bad inputs, inconsistent data, things breaking when real users touch them.

None of that matters until something actually runs. And this is where most projects never get. They stay in planning, structure, or "almost ready."

This one isn't because it's a great idea. It's because it runs.


The Uncomfortable Part

And honestly… it almost feels ridiculous saying that.

Because what's running right now is barely anything. There's no infrastructure, no scaling, no persistence.

It's not solving a massive problem.

It's just:

input --> output

That's it.


So the real question might become:

If this is so simple… why doesn't everyone do this first?

The answer isn't technical. It's psychological.


Simple work doesn't feel like progress. It feels small, incomplete, temporary—like something you're supposed to "build on top of" immediately.

So instead, we skip it and we jump straight into structure—services, hooks, abstractions, architecture; and make it look like a system before it actually is one.


How It Actually Happens

Even in this MVP, things started drifting.

I ended up defining a service interface:

import { Choice, PredictionState } from "../types/prediction";

export interface PredictionService {
  commit(choice: Choice): Promise<PredictionState>;
  getStatus(id: string): Promise<PredictionState>;
}

It looks reasonable. It even feels minimal. But step back for a second and see...

This already assumes there is a system managing state, predictions are asynchronous, results may not be immediate, and there's a concept of tracking by ID.

None of that is required to answer the original question:

Can we take an input and produce a prediction?

That's how subtle the shift is. You don't jump from simple to complex all at once.

You end up there one small step at a time.


What the MVP Actually Needed

If you strip everything back, it looks more like this:

type Choice = "A" | "B";

function predict(choice: Choice): string {
  return choice === "A" ? "Win" : "Lose";
}

That's it.

Input. Logic. Output.


This Is How Overengineering Happens

Not through big, obvious decisions, but through small, reasonable ones.

"We'll need a service anyway." "This should probably be async." "We might want to track status later."

Each step makes sense on its own. But together, they move you further away from the simplest working version.


The Difference That Matters

None of this means architecture is bad. Eventually, systems do need services, persistence, async workflows, retries, and observability.

But those should come from pressure, not anticipation.


That's the difference between:

engineering and speculative engineering


Where This Leaves Us

Right now, this MVP is laughably small. It doesn't do much. It doesn't have any infrastructure. It isn't scalable. It isn't "production ready."

But it runs.

And that puts it ahead of a surprising number of ideas that never leave the planning phase.


Code

If you want to see the exact MVP behind this:

https://github.com/NewJerseySoftware/mvp-evolution-series

No infrastructure. No abstraction layers. Just the working loop.