Technical Debt Killed My Project

A candid look at how unchecked technical debt and feature creep forced me to kill two major projects.

24 minutes

πŸ“„ Note: I Killed Two Projects. Here's Why I'm Glad.

So, recently, I killed two projects. Both were technically alive. Active commits, growing features, regular work. I was deep in them β€” especially one of them, which I'll admit was one of my favorite things I've ever built.

You might ask: Why kill something that's thriving?

The answer is technical debt. The silent, creeping kind. The kind you only see clearly when you step away.

I took a break β€” just one month β€” and when I came back, it hit me. The project was bloated. It was messy. It had grown so fast, and I had been so deep in the weeds, that I didn't notice how far I'd drifted from clean architecture, maintainable code, and long-term sanity.

Looking at it with fresh eyes? Brutal. It felt like returning to your childhood room and realizing you never cleaned up after the chaos.

This wasn't some side experiment either. I built this project like it mattered β€” because it did. It had so many features, so much potential. But all of that was buried under bad decisions, rushed implementations, and "I'll fix it later" code that never got fixed.

It was too much to handle. Refactoring wouldn't cut it. Rewriting parts wouldn't solve the underlying mess. I had to let it go.

Heartbreaking? Absolutely. But necessary.

I'll go into the gritty details later in this blog. I'll walk you through how it got here, what I learned, and what I'm doing differently next time. But for now, this post is about honoring the hard truth: sometimes, you don't salvage β€” you start over.

Not because you're weak. But because now, you know better.

How It All Started

This wasn't just any project. This was the project. One of my all-time favorites.

I started building it while playing Hypixel SkyBlock, frustrated by how many little problems I saw in the game. There were annoyances everywhere β€” things no mod seemed to fix β€” so I figured, screw it, I'll fix it myself.

That itch turned into a full-blown mission. Before I knew it, I had written almost 40,000 lines of Java. It grew fast. It was ambitious. It solved real pain points. And at the time, I genuinely loved working on it.

So, what went wrong?

The Rise of Technical Debt

Ironically, the project looked organized from the outside. Files were there. Things were working. But under the hood? A slow-moving disaster.

The real problem started when I got addicted to shipping features. Feature after feature after feature β€” more logic, more complexity, more chaos. I stopped tracking where things were. I couldn't remember which file imported what. Utilities were scattered. Some days, I'd just dump code into the first file I saw, thinking "I'll clean it later."

Spoiler: I never did.

Here are some concrete examples of what piled up:

  1. Spaghetti Code in the API Manager:

    • A single class handling authentication, rate limiting, and response parsing
    • 2,000+ lines of code in one file
    • Global state variables scattered throughout
    • No clear separation between HTTP client logic and business logic
  2. Config Management Nightmare:

    • One giant config file with 2,000+ lines
    • Hard-coded values mixed with dynamic settings
    • No validation or type safety
    • Circular dependencies between config sections
  3. Java-Kotlin Integration Chaos:

    • Mixed language boundaries with no clear separation
    • Inconsistent null safety handling
    • Type conversion issues between Java and Kotlin collections
    • Memory leaks from improper resource management
  4. Feature Dependencies:

    • Scanner code depending on unrelated UI components
    • Business logic mixed with presentation layer
    • No clear module boundaries
    • Circular imports between packages

The impact on maintainability was severe. Here's a specific example: When the API Manager bug surfaced, it took a week to fix what should have been a simple patch. Why? Because:

  1. The rate limiting logic was entangled with authentication
  2. Changes to one part broke three other features
  3. No tests existed to verify the fix
  4. Documentation was non-existent
  5. The code was so complex that debugging took days

Eventually, I reached the point where I'd open a file and have no idea what was going on. The architecture wasn't just messy β€” it was non-existent. A few thousand lines in a single class. Zero documentation. Circular dependencies. Patchwork fixes on top of patchwork fixes.

It was working, but barely. And I had no idea how to move forward without making it worse.

The Feature That Broke Me

At one point, things were still manageable. Yeah, there was some mess, but nothing I couldn't work around. The structure held β€” barely. I was still having fun.

But then I made a decision that wrecked everything:
I started building an Exotic Scanner.

If you're not familiar with Hypixel SkyBlock, here's the deal: "exotics" are armor pieces with colors that shouldn't exist. You can't dye items like that anymore, which makes those old items rare, aesthetic flexes. They're basically digital drip β€” niche, collectible, valuable.

So naturally, I thought, why not build a tool to scan for exotics in people's inventories or auction listings? Something to help me and others spot these rare pieces and maybe make some money off flipping them. Seemed simple enough.

That was the beginning of the end.

How the Exotic Scanner Blew Up the Codebase

The scanner began its life in version 1.2.5 as a simple feature. Little did I know it would evolve into a monstrous codebase. Here's the journey:

v1.0: The NBT Parser (The Spark)

  • Initial Goal: Detect exotic items in Hypixel SkyBlock
  • Implementation: Built a custom NBT parser for Minecraft item metadata
  • Data Collection: Three weeks of manual work cataloging every dye variant and rare item
  • Result: A solid but time-consuming foundation

v1.1: The API Manager

  • Integration: Connected to Hypixel API for live player data
  • Challenge: Massive JSON responses (thousands of lines per request)
  • Solution: Built a custom parser and Java object mapper
  • Cost: 8,000–10,000 lines of code for data handling

v1.2: Core Scanning Features

  • Basic item filtering
  • Exotic detection in inventories
  • Command system for scans and reporting
  • Status: Working, but the beginning of complexity

v1.3: Scanning Explosion

This is where technical debt began its exponential growth:

  • Lobby Scanning: Player detection and exotic gear flagging
  • Island Scanning: Deep co-op island analysis
  • Player & Co-op Scanning: Gear stat comparison
  • Cake Year Scanner: Cake bag content tracking
  • Item Filters: Cross-player item detection

The project had transformed from a simple mod into a comprehensive inspection toolkit.

v1.4: The GUI Problem

  • Features: Custom GUIs for filters, results, and item tags
  • Issues:
    • Performance-heavy real-time rendering
    • Lag in both game and development
    • Complex interaction handling

v1.5: The Utils Nightmare

  • Initial Goal: Create utility classes for common operations
  • Reality:
    • Utility classes became feature dumping grounds
    • Multiple responsibilities per class
    • Circular dependencies between utilities

❗ The Breaking Point

The system reached critical mass:

  • Unmanageable state tracking
  • Files ballooning from 800 to 2,500 lines
  • Complex import cycles
  • Fragile dependencies
  • Changes in one area breaking multiple features

The final result? A codebase that grew from 40,000 to 65,000 lines of Java, with approximately 25,000 lines dedicated to the scanner alone.

Why Feature Creep Without Architecture is a Death Sentence

The scanner's growth exposed several critical issues:

  1. No Clear Boundaries:

    • Scanner code leaked into core game functionality
    • UI components became tightly coupled with business logic
    • No separation between scanning, processing, and display logic
  2. Performance Issues:

    • Memory leaks from improper resource management
    • CPU spikes during scanning operations
    • UI freezes during heavy processing
  3. Testing Nightmare:

    • Impossible to test individual components
    • No clear entry points for unit tests
    • Integration tests became unreliable
  4. Maintenance Challenges:

    • Changes to one feature broke others
    • Bug fixes required understanding the entire system
    • No clear ownership of code sections

And the worst part? I couldn't maintain any of it. Not cleanly. Not confidently.

Everything was duct-taped together. Dependencies crossed wires. Scanner code relied on internal stuff that had nothing to do with it. Changes in one area broke others. I started fearing commits. Avoiding refactors. I felt trapped.

I thought about rolling back to v1.2.5 β€” the last "clean" version before this monster feature bloated the project. But I didn't. Why?

Sunk cost fallacy.

I had put in so much time. So much effort. So many late nights solving dumb bugs I created myself. The idea of just throwing that away? It felt like betrayal.

But the truth was: I had already lost control. I just didn't want to admit it yet

By this time, I didn't even want to commit anymore.

Every line felt risky. Every fix felt like it would break five other things I'd forgotten existed. And then came the final straw β€” not from the code, but from a friend.

One of my friends was using the mod and hit me up, frustrated. The API Manager wasn't calculating requests properly. The Config Manager was straight-up failing if you turned off certain options. Things that should've taken me hours to patch ended up taking a week β€” not because the bugs were complex, but because the technical debt turned every fix into surgery.

Nothing was isolated. Nothing was safe to touch.

Even worse? Some of the scanner features were so busted, I was scared to even open the files. I knew if I tried to fix one thing, it'd be like pulling a thread that unraveled the whole damn sweater.

And yet... I couldn't walk away.
Sunk cost fallacy.
I kept telling myself I had to make it work.
That it would be a waste to stop now.
That I owed it to the project β€” and to myself β€” to fix what I broke.

But deep down, I knew the truth:
The project didn't need a patch.
It needed a funeral.

The Hard Reset

Finally, on June 7th, I pulled the trigger. I deprecated the old project and decided to start over β€” from the ground up.

Sounds easy, right? Spoiler: it's not.

Starting a new project is the hardest part. When you've already built something big, you think you know what to do next. But when you actually face the blank slate? The "where do I even start?" moment hits like a ton of bricks.

For me, the first real task was a config manager.

My old project used something called OneConfig. This time, I wanted to try something new β€” a library called MoulConfig.

Great idea, in theory.

In reality? The lack of documentation made it an absolute nightmare.

It took me two days just to render a single category in the config. Not even to build the feature β€” just to get that one thing visible.

I swear, I wanted to quit more than once. A genuine plea to the MoulConfig devs:
Please. Write. Docs.

I love the power of the library, but the frustration nearly broke me.

One saving grace? I decided to write this part in Kotlin. I'd only learned Kotlin a few months earlier β€” maybe two or three β€” and I was itching to put it into practice.

So yeah, while I was knee-deep in config hell, at least I was getting some good Kotlin reps.

Mixing Kotlin and Java Hell

After replicating the project groundwork, I ran into a brutal roadblock: zero documentation for Moldberry. Seriously, just a simple annotation, nothing more. For two days, the config just wouldn't render. I was stuck.

Finally, I cracked it: I had to use Java inside the Kotlin project.

Wait, what?

Most of the Kotlin projects I've worked on were pure Kotlin β€” clean, simple. But here, I was forced to mix Kotlin and Java. And that turned out to be a nightmare.

Every time I imported Java into Kotlin, something broke elsewhere. It was a tangle of mismatched expectations, bugs popping up like weeds. Trying to switch from the main config to the essential config? Spent a full day wrestling that mess, to no avail.

The whole experience felt like technical debt all over again β€” but this time, preemptively.

I didn't want to repeat the same mistakes. I didn't want to end up with a Frankenstein's monster of Java-Kotlin mashups. I just wanted something that works β€” clean, maintainable, and without the headache of chasing bugs born from language clashes.

The starting point was brutal. Setting up imports and configs felt like wading through mud. But I knew this was the hardest step. Once the foundation was stable, I could iterate and build up without that crushing weight.

Starting is always the hardest part β€” whether you're coding or cleaning up legacy mess.

Building Smarter: Lessons Learned and The Road Ahead

I wasn't about to let history repeat itself.

This time, I promised myself: no shortcuts. No messy hacks. No piling up technical debt like a ticking time bomb.

That meant:

  • Clear separation of concerns from day one. Every module, every feature had its place and scope.
  • Thorough documentation, even if it's just me reading it later. No more silent frustrations hunting down "why does this break?"
  • Using Kotlin fully where possible β€” no more half-baked mixes of Java and Kotlin causing mysterious bugs.
  • Building small, incremental features instead of dumping everything into one giant monolith.
  • Designing the project structure with future maintenance in mind, not just quick wins.

I'm treating this as a marathon, not a sprint.

Yes, it's frustrating and slow at times β€” especially when starting from scratch is such a grind β€” but it's necessary.

Because a clean foundation lets you build confidently. It keeps the project alive long after the initial excitement fades. It means you spend your time writing new features, not chasing ghosts from the past.

This is how I'm moving forward.

The old project was a lesson β€” brutal, painful, but invaluable. Now, with every line of Kotlin, every config I render, I'm building not just software but a mindset that respects code, process, and sanity.

No more tech debt death traps.

Just smart, sustainable progress.

"Technical debt kills projects."

Simple. Brutal. True.

It's the silent assassin behind every abandoned codebase and every burned-out developer.

You don't see it piling up day-to-day β€” until one day, it's too heavy to carry.

Tools & Libraries: OneConfig vs MoulConfig

When I started rebuilding, I had to choose between two configuration libraries: OneConfig and MoulConfig. Here's how they stack up:

OneConfig

Pros:

  • Well-documented and supported by a strong team
  • Everything is straightforward and easy to implement
  • Great for monolithic configurations
  • Strong type safety and validation
  • Built-in UI components
  • Active community support

Cons:

  • The UI isn't as customizable as I'd like
  • Monolithic design can lead to massive files (I had 2,000 lines in one config file)
  • Hard to maintain and understand as the project grows
  • Limited modularity
  • Performance issues with large configurations
  • No built-in versioning

MoulConfig

Pros:

  • Modular design allows for easy module imports and changes
  • More flexible and scalable for larger projects
  • Better performance with large configurations
  • Built-in versioning support
  • Highly customizable UI
  • Better separation of concerns

Cons:

  • Poor documentation made it a nightmare to implement
  • It took me two days just to render a single category
  • Integration issues between Kotlin and Java caused unexpected bugs
  • Steeper learning curve
  • Less community support
  • More boilerplate code required

The Impact of Poor Documentation

The lack of documentation for MoulConfig had severe consequences:

  1. Development Time:

    • Two days to implement a single category
    • Three days debugging integration issues
    • One week to get basic functionality working
    • Total: 2x longer than estimated
  2. Code Quality:

    • Had to reverse engineer the library
    • Made assumptions that later proved wrong
    • Created workarounds that became technical debt
    • No clear best practices to follow
  3. Maintenance Issues:

    • Hard to understand the code later
    • Difficult to upgrade the library
    • No clear upgrade path
    • Documentation gaps led to bugs

Kotlin Adoption and Java-Kotlin Integration Pain

The decision to use Kotlin brought its own set of challenges:

  1. Language Integration Issues:

    • Null safety conflicts between Java and Kotlin
    • Collection type mismatches
    • Different exception handling patterns
    • Memory management differences
  2. Build System Complexity:

    • Mixed language compilation
    • Different dependency management
    • Build time increased significantly
    • More complex CI/CD pipeline
  3. Development Workflow:

    • Different IDE support
    • Mixed code style guidelines
    • Inconsistent tooling
    • Team knowledge gaps
  4. Performance Considerations:

    • Runtime overhead from language interop
    • Memory usage differences
    • Garbage collection impact
    • Startup time increase

The experience taught me valuable lessons about language choice and integration:

  1. Clear Boundaries:

    • Define clear interfaces between languages
    • Keep language-specific code isolated
    • Use proper interop annotations
    • Document language boundaries
  2. Build Process:

    • Set up proper build tools
    • Configure IDE support
    • Establish coding standards
    • Create clear documentation
  3. Team Considerations:

    • Assess team expertise
    • Plan for knowledge sharing
    • Consider maintenance impact
    • Evaluate long-term costs

These challenges reinforced the importance of proper tool selection and documentation in software development.

Mental & Process Lessons

The Sunk Cost Fallacy in Software Projects

I fell into the trap of sunk cost fallacy. I had invested so much time and effort into the old project that I couldn't let it go, even when it was clear it was beyond repair. It's a common pitfall in software development, but recognizing it is the first step to moving forward.

The sunk cost fallacy manifested in several ways:

  1. Time Investment:

    • "I've spent 6 months on this, I can't give up now"
    • "All those late nights would be wasted"
    • "I've already fixed so many bugs"
  2. Emotional Attachment:

    • "This is my baby, I can't abandon it"
    • "I know every line of code"
    • "It's part of my identity as a developer"
  3. Fear of Starting Over:

    • "What if the new version isn't as good?"
    • "I might make the same mistakes"
    • "The community might not like the new version"
  4. Pride and Ego:

    • "I can fix this, I just need more time"
    • "Other developers would give up, but I won't"
    • "I've never failed before, I won't start now"

The Importance of Knowing When to Kill vs Fix

Sometimes, the best decision is to start over. It's not a sign of weakness but of wisdom. Knowing when to kill a project versus trying to fix it can save you from endless frustration and wasted time.

Key indicators that it's time to start over:

  1. Technical Signs:

    • Every bug fix creates two new bugs
    • No one understands the codebase
    • Build times are increasing exponentially
    • Tests are unreliable or non-existent
  2. Process Signs:

    • Development velocity is decreasing
    • Team morale is low
    • Technical debt is growing faster than features
    • Documentation is outdated or missing
  3. Business Signs:

    • Features take longer than expected
    • Customer satisfaction is decreasing
    • Maintenance costs are rising
    • New requirements are hard to implement

How I Shifted My Mindset from Hacking to Disciplined Building

I've learned to prioritize clean architecture and maintainable code over quick wins. It's a shift from hacking to disciplined building, and it's made all the difference.

The mindset shift involved several key changes:

  1. Planning Over Hacking:

    • Design before implementation
    • Document before coding
    • Test before shipping
    • Review before merging
  2. Quality Over Speed:

    • Write clean, maintainable code
    • Follow consistent patterns
    • Use proper abstractions
    • Maintain clear boundaries
  3. Long-term Over Short-term:

    • Consider future maintenance
    • Plan for scalability
    • Document decisions
    • Build for change
  4. Process Over Product:

    • Establish clear workflows
    • Define coding standards
    • Create review processes
    • Maintain documentation

This shift wasn't easy, but it was necessary. It required:

  1. Self-awareness:

    • Recognizing bad habits
    • Accepting past mistakes
    • Learning from failures
    • Embracing change
  2. Discipline:

    • Following processes
    • Maintaining standards
    • Writing tests
    • Documenting code
  3. Patience:

    • Taking time to design
    • Building properly
    • Testing thoroughly
    • Reviewing carefully
  4. Humility:

    • Accepting feedback
    • Learning from others
    • Admitting mistakes
    • Asking for help

The result? A more sustainable, maintainable, and enjoyable development process. One that doesn't lead to burnout or technical debt. One that allows for growth and improvement. One that builds better software.

Best Practices Moving Forward

Incremental Builds

Building small, incremental features instead of dumping everything into one giant monolith helps maintain clarity and control. Here's how to implement this approach:

  1. Feature Planning:

    • Break down features into small, manageable chunks
    • Define clear acceptance criteria
    • Set realistic timelines
    • Plan for testing and documentation
  2. Implementation Strategy:

    • Start with core functionality
    • Add features one at a time
    • Test each addition thoroughly
    • Document as you go
  3. Review Process:

    • Regular code reviews
    • Performance testing
    • Security checks
    • Documentation updates
  4. Deployment Strategy:

    • Small, frequent releases
    • Feature flags for new functionality
    • Rollback plans
    • Monitoring and metrics

Modular Design and Separation of Concerns

Every module and feature should have its place and scope. This ensures that changes in one area don't break others.

  1. Architecture Principles:

    • Clear module boundaries
    • Well-defined interfaces
    • Dependency management
    • Error handling
  2. Code Organization:

    • Logical file structure
    • Consistent naming conventions
    • Clear package hierarchy
    • Proper use of design patterns
  3. Testing Strategy:

    • Unit tests for modules
    • Integration tests for interfaces
    • End-to-end tests for features
    • Performance tests for critical paths
  4. Documentation Requirements:

    • Module purpose and scope
    • Interface specifications
    • Usage examples
    • Known limitations

Documentation Discipline

Even if it's just notes for yourself, thorough documentation is essential. It saves you from future headaches and helps others understand your code.

  1. Code Documentation:

    • Clear function and class documentation
    • Parameter descriptions
    • Return value explanations
    • Usage examples
  2. Architecture Documentation:

    • System overview
    • Component interactions
    • Data flow diagrams
    • Deployment architecture
  3. Process Documentation:

    • Development workflow
    • Testing procedures
    • Deployment process
    • Troubleshooting guides
  4. Maintenance Documentation:

    • Common issues and solutions
    • Performance optimization tips
    • Security considerations
    • Upgrade procedures

Tooling Choices with Future Maintenance in Mind

Choose tools and libraries with future maintenance in mind. Consider the long-term impact of your choices.

  1. Library Selection Criteria:

    • Active maintenance
    • Good documentation
    • Strong community
    • Clear upgrade path
  2. Build System Requirements:

    • Fast build times
    • Clear dependency management
    • Good IDE integration
    • CI/CD support
  3. Testing Framework Needs:

    • Easy to write tests
    • Good coverage reporting
    • Performance testing support
    • Mocking capabilities
  4. Monitoring and Logging:

    • Error tracking
    • Performance monitoring
    • Usage analytics
    • Debug capabilities

A Roadmap for the Rebuild

How I'm planning to structure the new project and what I'm doing differently this time to avoid the same traps.

  1. Project Structure:

    • Clear module boundaries
    • Consistent naming conventions
    • Proper package organization
    • Documentation structure
  2. Development Process:

    • Code review requirements
    • Testing requirements
    • Documentation requirements
    • Release process
  3. Quality Assurance:

    • Automated testing
    • Code quality checks
    • Performance testing
    • Security scanning
  4. Maintenance Plan:

    • Regular updates
    • Dependency management
    • Documentation updates
    • Performance monitoring

By following these practices, I'm building a foundation for sustainable development. One that allows for growth, maintains quality, and prevents technical debt from accumulating. It's not just about writing code β€” it's about building software that lasts.

🧠 Meta Technical Debt: This Blog Itself Funny thing? The very blog you're reading right now is its own example of technical debt.

I’ve rewritten this site three times already.

First, it was built in React + MDX.

Then I switched to Contentlayer for better MDX parsing.

Then I ported it into Next.js with custom routing and MDX enhancements.

Now it's a Frankenstein that works… mostly.

This site is full of technical content, but ironically, it's also a graveyard of uncommitted refactors. I only use it to write β€” I haven’t committed anything beyond blog posts in months. Not because it’s perfect, but because I know one day I’ll nuke it and rebuild it from the ground up. Again.

Mark my words: This blog will be rewritten. Just not today.

Because today, I’m too busy fighting technical debt elsewhere.