Fancy or Fragile? How to Hit the Sweet Spot
The Dangers of Over and Under-Engineering in < 5 minutes
Have you ever launched a feature you spent way too much time perfecting?
Or maybe released something that later was difficult to extend and had performance issues?
Guilty. đ
Building software is all about finding the perfect balance.
But where is that sweet spot, and how do you avoid the pitfalls of over and under-engineering?
âď¸ Main Takeaways
- Start with a clear MVP: Focus only on essential features. 
- Iterate based on user feedback: Continuously improve and expand features. 
- Prioritize maintainability: Write clean, well-documented code. 
- Avoid unnecessary complexity: Keep designs simple and practical. 
Letâs look at the arguments for under and over-engineering and my tips on finding the sweet spot.
In last weekâs issue, I used the example of a Wizard UI component to demonstrate my point of AI-driven coding:
Well, guess what!
Iâve built the entire component, and despite starting from the template offered by the AI, after a few iterations, we came to a completely different solution.
đ The Simple Solution
This example perfectly demonstrates the problem of under-engineering.
// logic for back, next finish
<>
  <Stepper activeStep={currentStep} orientation="horizontal">
    {steps.map((child, index) => (
      <Step key={index}>
        <StepLabel>{child.props.label}</StepLabel>
      </Step>
    ))}
  </Stepper>
  <div>
    {renderStepContent()}
    <Button variant="contained" disabled={currentStep === 0} onClick={back}>
      Back
    </Button>
    <Button variant="contained" onClick={currentStep === steps.length - 1 ? finish : next}>
      {currentStep === steps.length - 1 ? 'Finish' : 'Next'}
    </Button>
  </div>
</>And in the code, Iâd use this component like this:
<Wizard>
  <Step1Component label="Connect account" />
  <Step2Component label="Trigger Events" />
  <Step3Component label="Actions" />
  <Step4Component label="Verification" />
</Wizard>Pretty simple, right?
Yes, but what if:
- I donât want a Back and Finish/Next button on all of my wizards. 
- I want to be able to control the wizard steps programmatically 
- I want to skip steps 
- I want to render the wizard controls in a different place 
These are all possible use cases for a wizard component that should be reusable. But despite taking minutes to create, our simple solution doesnât allow any extension.
So, to recap:
đŞ Arguments for Under-Engineering
Pros:
- Speed of Development - Quick to Market: Less time spent on unnecessary features means faster delivery. 
- Reduced Costs: Less development time reduces initial project costs. AI makes this trivial. 
 
- Simplicity - Easier Maintenance: Simpler code is often easier to understand, debug, and maintain. 
- Flexibility: Leaner codebases are more adaptable to changing requirements or pivots. 
 
Cons:
- Technical Debt - Accumulating Issues: The next team using your reusable component will need new features for their use cases. The more teams try to adapt your component, the more issues youâll have to address. 
- Hidden Bugs: Patching a design that wasnât robust is a recipe for disaster. Such patches will result in undetected bugs that cause bigger problems or inflexibilities in the future. 
 
- Scalability and Performance - Limited Scalability: Under-engineered solutions might not handle increased load or complexity well. 
- Potential Rewrites: As demands grow, the initial simplistic design may need a complete overhaul. 
 
đ¤ The Robust Solution
So we took a step back and arrived at a solution that:
- extracted the wizardâs âstepperâ logic from the wizardâs display 
- wizard controls are added per use-case, with no hardcoded buttons 
const steps = [{
  label: "Connect account",
  component: <Step1Component />
  isDisabled: false,
  ...
}, { step2...
}, { step3...
}];
const {
  back, next, finish, isFirstStep, isLastStep, setCurrentStep
} = useWizard(steps);
<>
  <Wizard steps={steps} />
  {!isFirstStep && <button onClick={back}>Back</button>}
  {isLastStep ?
    <button onClick={finish}>Finish</button> :
    <button onClick={next}>Next</button>}
  }
</>This solution takes the case of all of our previous problems:
- you can add Back, Next, Finish as you please 
- you can jump to a specific stepâmeaning you can also skip steps 
- how you render - <button>is up to you because itâs not part of the wizard
But it also brings up a couple of other problems, for example:
- repeating the Back/Next/Finish logic every time 
- needing extra logic to find the next/previous step if some steps are disabled 
- you have two functions instead of one ( - useWizardand- Wizard) to maintain, as well as all the other components that will use- Wizardand the Back/Next/Finish logic
đ°ď¸ Arguments for Over-Engineering
Pros:
- Robustness and Reliability - Fewer Bugs: More rigorous design and testing can result in more stable software. In a separation like this, you can test the business logic of the wizard and the wizard separately and together. 
- Future-Proofing: Extracting the logic made a reusable component more adaptable to growth and new features. 
 
- Scalability - Handles Growth Well: Unlike The Simple Solution, a little over-engineering handles future complexity better. 
- Built-In Flexibility: Revrites become nonissues because the current design is flexible enough to allow for easy feature additions. 
 
Cons:
- Increased Complexity - Complex Maintenance: This is much more difficult to understand than The Simple Solution. It has more moving parts, leading to longer debugging and development times. 
- Steep Learning Curve: New team members might struggle to get up to speed. 
 
- Cost and Time - Higher Costs: More resources are needed upfront, increasing the projectâs costâWe worked more on this than we initially estimated. đ 
- Delayed Launch: Thankfully, no launch depended on this particular component, but in a different scenario, we might have to settle for something less robust. Spending too much time on details can delay the productâs time to market, missing opportunities. 
 
So, what can we do?
âŻď¸ Finding the Balance
- Define Clear Requirements: - Focus on MVP (Minimum Viable Product): Prioritize features essential for the initial release. 
- Iterative Development: Based on user feedback and real-world use cases, plan to enhance and add features in future iterations. Donât do a customizable Wizard layout if no team needs it. 
 
- Set Realistic Goals: - Time and Budget Constraints: Itâs difficult to justify spending time and money on features you donât need, but also consider: 
- Risk Assessment: Evaluate the risks of not implementing certain features now versus the potential costs of implementing them prematurely. This is where potential rewrites (and their cost đŹ) come into play. 
 
- User Feedback and Testing: - Beta Testing: Release beta versions to gather user feedback and identify necessary features. 
- User-Centric Design: Focus on building features that directly address user needs and pain points. 
 
- Simplify Designs: - Avoid Perfection: Resist the urge to add extra features or overly complex solutions that donât provide significant value. 
- Refactor Code Regularly: Periodic code reviews and refactoring can help keep the codebase clean and maintainable without unnecessary complexity. 
 
How do you avoid perfecting a feature but not making it too dumb that it needs to be rewritten every time someone touches it?
đ° Weekly shoutout
đŁ Share
Thereâs no easier way to help this newsletter grow than by sharing it with the world. If you liked it, found something helpful, or you know someone who knows someone to whom this could be helpful, share it:
đ Subscribe
Actually, thereâs one easier thing you can do to grow and help grow: subscribe to this newsletter. Iâll keep putting in the work and distilling what I learn/learned as a software engineer/consultant. Simply sign up here:




Looks like we were thinking about this topic at the same time, the same week I released this article: https://www.leadership-letters.com/p/the-sweet-spot-between-too-much-and, consider checking it out!
Loved your post, btw.
A great mental model Akos! It can help decide between over-engineering and under-engineering, though I won't call your second solution over-engineering :) It was a perfectly reasonable thing to do as far as I can see. Sure it took more time but the wizard seems like a pretty basic building block that can come in handy (depending on the system context, of course).
Most of the time I've seen the decision between making something flexible enough for the future boils down to how much time you have. It's always a trade-off.
Also, thanks for the mention!