The Irony of Greenfield Projects: Why Your Fresh Codebase Becomes a Nightmare

The Irony of Greenfield Projects: Why Your Fresh Codebase Becomes a Nightmare
Gojo's Unlimited Void (infinite freedom) vs Sukuna's Malevolent Shrine (infinite chaos) – when you have unlimited freedom to build, bad practices inevitably come with it

Cover Image Credits: motionbgs.com ↗

When you join a new company, you know you are praying hard to be assigned to either a brand new project or a relatively recent one (Including myself). We all know how traumatizing months or years old codebases can be. Inconsistent patterns, tangled dependencies, components nobody dares to touch, security vulnerabilities buried deep, you name it. We all can agree that most of the time it’s utter chaos.

The thing is, they were once brand new code bases as well. Just because you have a new project, doesn’t mean you won’t end up in the dark side. You might already be en route to be doomed.

sith-meme

The reality is painful. You had the freedom you asked for. You had the chance to do it right. You researched thoroughly, made informed decisions about your tech stack. Yet, six months or a year later, you’re staring at unmaintainable code that nobody wants to touch.

Who do you think the culprit is?

Probably not just someone or something. It’s a collection of everything. Researching and picking a solid tech stack is necessary, but the process just doesn’t end there. Infact it’s just half of the process. Just because you chose battle tested technologies and libraries, that are/were popular at the time doesn’t mean you’re set up for success.

What actually determines whether your codebase thrives or becomes unmaintainable?

Simple answer is Team alignment on practices.

Unopiniated technologies (most web frameworks) are double edge sowrds. Without explicit, enforced conventions, without a shared understanding of how you’ll use the tools, your freedom becomes fragmentation. Everyone writes code the way they’re used to or most comfortable with. That paves the road for multiple faliure points. Style clashes. Patterns diverge. When business pressures hit (and they always do), you don’t have the solid foundation to adapt. Instead, you have a house of cards.

The good news? This is entirely preventable.


Here’s what I’ve noticed working on greenfield projects

To be accurate and rely on my experience, I’m going to talk about a scenario of a Web Development project.

It’s month one. You’re excited. The architecture is solid. The conventions are clear (or so you thought). Everyone’s aligned.

Fast forward to month six. You pull up a random component. It’s defined one way. You pull up another. The structure is completely differently. The state management is hell. The anti-patterns have taken over the roots. Styling is all over the place with multiple methods. The worst part is, we are still wondering:

What the hell happened here

After jumping between a few of these projects in my career, the pattern repeats itself. The names change, the tech stacks differ, but the breakdown looks remarkably similar. Welcome to the most common tragedy in greenfield projects. And I’m willing to bet you’ve seen it before or you’re living it right now.

Let me walk you through what usually happens:


The Myth of “Getting the Stack Right”

Let me get something straight. There’s no universally “correct” tech stack. What we can do is, finding tools or libraries that go well together. Whether you choose NextJS or Svelte, Tailwind or styled-components (R.I.P), these are tools. Tools don’t enforce behavior.

What matters is what your team agrees to do with them.

Here’s a scenario that plays out in almost every greenfield project:

You spend weeks researching. The team collectively decides on modern, solid technologies. Everyone nods in agreement. Architecture review is done. You’re set.

Then development starts.

Two weeks in, you realize nobody actually agreed on:

  • How to structure your code. Some developers follow one pattern, others follow another. Some build reusable modules, others write large monolithic files.
  • How to approach styling. One developer uses Tailwind. Another uses CSS Modules. A third uses styled-components. Someone else, frustrated with configuration, just does it inline when they’re too busy.
  • How to manage state. Guidelines were documented, but developers bypass them in multiple places. They update shared state from different locations. Nobody’s sure what the actual data flow is.
  • Code organization. Naming conventions? Project structure? Documentation standards? Sure, you discussed them once. Now, everyone’s doing their own thing.

This isn’t incompetence. This is the default behavior when teams have freedom but no guardrails.


Where It Falls Apart

The Tooling Fragmentation Problem

When multiple approaches exist side-by-side, you end up with:

  • Duplicate logic because nobody knows what patterns already exist
  • Conflicting implementations where the same problem is solved differently across the codebase
  • Upgrade hell when you try to migrate from one approach to another, you’re untangling code scattered across dozens of files

The Anti-Pattern Freedom

Here’s where greenfield projects really go sideways. When teams lack explicit alignment on how to use their tools, developers gravitate toward patterns that work but aren’t right. These anti-patterns aren’t syntax errors. They don’t break builds. They’re subtle, insidious, and they compound over time.

Let me walk you through the most common ones I’ve seen destroy codebases:

Styling Anti-Patterns: When Utility Classes Stop Being Utilities

Tailwind is a fantastic tool. probably the best thing happened to Web development after React. When used as intended, it makes DX much better. But without team conventions, you’ll see:

The @apply Overuse Anti-Pattern

Using Tailwind’s @apply directive extensively is considered an anti-pattern by the Tailwind team itself.¹ When developers use @apply for every component instead of composing utility classes directly in markup, you’re essentially writing CSS with extra steps. You lose:

  • The ability to see styles at a glance in your markup
  • Tree-shaking benefits because you’ve created custom CSS bundles
  • The constraint that makes Tailwind powerful—composition over abstraction

Example of the anti-pattern:

/* styles.css - The Wrong Way */
.card {
  @apply p-4 rounded-lg shadow-md bg-white;
}

.card-header {
  @apply text-xl font-bold mb-2;
}

.card-body {
  @apply text-gray-700;
}

Now you’ve recreated traditional CSS architecture. Workarounds stack on workarounds. Good luck refactoring that later without breaking something.

The Inline Style Escape Hatch

When developers hit a wall with utility classes, they reach for inline styles:

<div style={{ marginTop: `${spacing * 2}px`, color: isDark ? '#fff' : '#000' }}>

This works. But now you’ve got:

  • No consistency with your design system
  • Dynamic values that should be CSS variables
  • Specificity issues when trying to override these styles elsewhere

The Mixed Methodology Nightmare

CSS Modules in one component. Tailwind in another. Styled-components in a third. Inline styles when someone’s frustrated.

The codebase becomes a museum of styling approaches. Every new developer has to learn all of them. Refactoring becomes guesswork.

State Management Anti-Patterns: The Redux Chaos

Redux (or any state management library) gives you power. Without conventions, that power creates chaos. Like Uncle Ben once said; uncle-ben

The “Everything in Redux” Anti-Pattern

Not all state belongs in global stores. But without clear guidelines, developers default to putting everything there:

  • Form input values that should be local state
  • UI toggle states (modals, dropdowns) that never need to be shared
  • Derived data that should be computed from existing state
  • Server state must probably be handled by something like React-Query

Result? Your Redux store becomes a dumping ground. Actions balloon. Reducers sprawl. The single source of truth becomes the single source of complexity.

The “Update Anywhere” Anti-Pattern

When there’s no guardrail on how to manage state, developers bypass established patterns in multiple places, updating the same shared state from different locations.

// Component A directly dispatches
dispatch(updateUser({ ...user, name: 'New Name' }));

// Component B calls a thunk
dispatch(fetchAndUpdateUser(userId));

// Component C mutates (with Immer) in a different way
dispatch(userSlice.actions.patchUser({ name: 'Another Name' }));

Now you have:

  • Concurrency issues when multiple async requests complete in unexpected order, stale data overwrites fresh data in the store. In single threaded environments like JavaScript, concurrent network requests finishing out of sequence cause the UI to display outdated information.
  • Impossible to reason about data flow because updates happen everywhere
  • No clear ownership of state transitions
  • Debugging nightmares because you can’t trust which action caused the state change

The Performance Killing State Selection Anti-Pattern

Whether you’re using Redux, Zustand, Jotai, or any other state management solution, the way teams select and derive state often becomes a silent performance killer. Without clear conventions, developers create patterns that work but destroy performance at scale.

Common anti-patterns include:

  • Selecting entire state objects and computing derived data in components—every render recalculates, even when the underlying data hasn’t changed
  • Building deep dependency chains of selectors fragile 5+ level chains where changing one selector means debugging five others
  • Not using memoization at all expensive computations run thousands of times per second
  • Broken memoization patterns creating new selector instances on every render, defeating the entire purpose of memoization
  • Computing derived state at render time filtering, sorting, and transforming large datasets in component bodies instead of selector functions

The real-world impact is brutal:

  • Initial load with 50-100 items: Everything feels fast
  • After 1,000 items: Minor lag, 200ms delays when typing
  • After 10,000 items: Noticeable freezing, 1-2 second hangs on interactions
  • After 50,000 items: App becomes unusable, browser tab crashes

Performance profiling shows components re-rendering 30+ times per second. State selection logic runs thousands of times. But because the patterns are scattered across dozens of files, some properly optimized, some not, some with broken memoization; nobody knows where to start fixing it.

Teams discover the problem too late. By the time performance issues surface, the codebase has hundreds of components using different selection patterns. Refactoring means touching the entire application. The technical debt compounds faster than anyone can address it.

React/Next.js/TypeScript Anti-Patterns: The “It Compiles” Trap

TypeScript gives you type safety. React gives you component composition. Next.js gives you full-stack patterns. But these tools don’t enforce how you use them.

The any Escape Hatch

Deadlines hit. TypeScript complains. Developers reach for the escape hatch:

const handleSubmit = (data: any) => {
  // TODO: Fix types later
  api.post('/endpoint', data as any);
};

This works. TypeScript stops complaining. But you’ve just thrown away type safety. Six months later, someone changes the API contract, and runtime errors cascade through your app because types lied.

The Prop Drilling Olympic Games

Without clear patterns for state management, developers pass props through 4-5-6 component layers:

<GrandParent user={user}>
  <Parent user={user}>
    <Child user={user}>
      <GrandChild user={user}>
        {/* Finally used here */}
      </GrandChild>
    </Child>
  </Parent>
</GrandParent>

This works. But now:

  • Refactoring requires touching multiple files
  • Components become tightly coupled to parent data they don’t use
  • Testing requires mocking data through multiple layers

The right approach? Context, composition patterns, or proper state management. But without team alignment, prop drilling becomes the default.

The useEffect Spaghetti

React’s useEffect is powerful but dangerous. Without conventions, you see:

useEffect(() => {
  fetchData();
  updateLocalStorage();
  trackAnalytics();
  
  return () => {
    cleanup();
    moreCleanup();
  };
}, [dep1, dep2, dep3, dep4]);

Multiple concerns in one effect. Dependency arrays that are wrong (or missing). Effects that fire on every render. Effects that cause infinite loops.

This compiles. But your app has performance issues, memory leaks, and race conditions nobody can debug.

The Next.js “Use Client” Everywhere Anti-Pattern

Next.js 13+ introduced the App Router with server components by default. But when developers hit issues, they slap "use client" on everything:

'use client'; // At the top of every file because it's easier

export default function Page() {
  // Could have been a server component
}

Now you’ve lost:

  • The performance benefits of server components
  • Reduced client bundle size
  • The whole point of the App Router

The Bloated Component Problem

When there’s no agreement on component structure or size, components grow unchecked. They become “Gods” that do everything: handle business logic, manage forms, fetch data, update stores, render UI.

function UserDashboard() {
  // 500 lines of state declarations
  // 300 lines of useEffect hooks
  // 200 lines of event handlers
  // 400 lines of JSX with nested ternaries
  
  return (/* Chaos */);
}

Now you’ve got a massive file that:

  • Nobody wants to touch because changing one thing might break five others
  • Can’t be tested in isolation²
  • Can’t be reused elsewhere in the app
  • Becomes a dumping ground for new features because the path of least resistance is adding to the existing monster

Why These Anti-Patterns Persist

None of these patterns break your app. They work. Builds succeed. Features ship.

But they’re technical time bombs:

  • Upgrading becomes a nightmare because changing patterns means finding and fixing code scattered across the codebase
  • Onboarding new developers takes weeks because they have to learn all the inconsistent patterns
  • Velocity crashes as the codebase grows because nobody can reason about it anymore
  • Security vulnerabilities can’t be patched cleanly because the codebase has no consistent patterns

Like I said time and time, the tool wasn’t the problem. The lack of shared practices was.


The Hidden Cost: Upgrade Paralysis

Well, now we have successfully transformed our greenfield into a complete mess, let me tell you how it will come back to haunt you.

Will all or some of those anti-patterns in the code, when you need to upgrade, you realize some of those workflows are no longer supported in the newer versions of the libraries/frameworks (I mean we were not supposed to use them that way to begin with).

  • Framework major version? Some components use old patterns, some use new ones. Upgrade breaks both.
  • Dependency update? The custom implementations you wrote don’t work anymore. The shortcuts don’t need updating. The different approaches need investigation.
  • Library updates? The 10 different ways your team uses your tooling means 10 different migration paths.
  • Security vulnerability in a dependency? You can’t upgrade cleanly because nothing in your codebase follows consistent patterns. SO if the library has a fix for a major security vulnerability in a newer version, god speed soldier.

What should be a 2-3 day task becomes a two-week nightmare. And if the vulnerability is critical, you’re stuck between a rock and a hard place: upgrade and risk breaking the world, or stay vulnerable.


Who’s Actually Responsible?

Here’s the uncomfortable truth. This doesn’t happen purely because developers are lazy or incompetent.

External forces contributed:

  • Business logic changes. Requirements shifted. The initial architecture made sense; now it doesn’t. But instead of revisiting patterns, teams just bolt features on.
  • Deadline pressure. “We need this by Friday.” Taking shortcuts feels like the only option.
  • Leadership unclear on direction. If there’s no clear ownership of code quality, developers optimize for speed instead.
  • Stakeholder misalignment. Product wants features. Engineering knows the codebase is fragile. Nobody has authority to say “we pause features and refactor.”

All of these factors compound the lack of team alignment. When business pressure hits, teams fracture further. Each developer solves the problem in front of them, never coordinating with others.

tech-debt-lol


What Actually Prevents This

The simple answer is, establishing practices from day one.

1. Explicit Conventions Over Tools

Before writing a single line of code:

  • Decide how you structure code. Not “suggestions.” Actual rules.
  • Pick one approach to styling. Not “use what you’re comfortable with.” One tool, one way.
  • Document your patterns. How do you organize state? When do you use local patterns vs. global? No exceptions.
  • Establish code organization standards. File naming, folder structure, where logic lives.

Write these down. Review them in code reviews. Enforce them.

This takes a day or two upfront. It saves months of pain later.

2. Code Review Culture

Code reviews aren’t just about catching bugs. They’re about enforcing conventions.

If someone deviates from agreed patterns, the review catches it. Early. Before the deviation spreads.

This means:

  • Reviews actually slow down development initially (days become a week for some PRs)
  • But velocity increases later because the codebase remains navigable
  • New developers onboard faster because patterns are consistent

3. Refactoring Discipline

Conventions slip. Code accumulates shortcuts. This is normal.

But if you allocate 10-20% of sprint time to refactoring before the debt piles up, you prevent the avalanche.

Small, consistent refactoring keeps the codebase healthy. Large, panicked refactors (or avoiding them entirely) is what creates unmaintainability.

4. Stakeholder Alignment

Someone needs to communicate to business stakeholders: “Quality now = speed later.”

Skipping refactoring and conventions to deliver features faster creates short-term wins and long-term pain. Good leadership explains this trade-off and manages expectations.

If every sprint is 100% features with 0% practices, your project’s velocity will crash in 6-12 months. Stakeholders need to understand this.


The Real Lesson

Greenfield projects fail not because of technology choices. They fail because teams assume agreement on tools = agreement on practices.

It doesn’t.

The irony of having complete freedom in a greenfield project is that without constraints, teams create chaos. Not maliciously. Not from incompetence. Simply because humans work better with clear guidelines.

Here’s what prevents your brand new codebase from becoming a nightmare:

  1. Explicit, documented conventions before coding starts
  2. Rigorous code review to enforce those conventions
  3. Consistent allocation of time for refactoring and technical health
  4. Leadership that prioritizes long-term sustainability over short-term velocity
  5. Stakeholder buy-in that quality now = speed later

None of this is sexy. None of it shows up in sprint reviews. But it’s the difference between a codebase that’s a joy to work on and one nobody dares touch.

Your tech stack choice matters. But your team practices matter far more.


Closing Thoughts

Greenfield projects offer freedom and a blank slate, a chance to “do it right.” Yet many teams end up with unmaintainable codebases within months because they conflated picking good tools with establishing good practices.

Without explicit conventions on code structure, styling approaches, state management, and code organization, each developer codes how they’re used to. Fragmentation sets in. When business pressures hit, the lack of foundation shows. Components balloon to massive sizes. Multiple approaches clash. State management becomes chaotic. Security vulnerabilities can’t be patched cleanly because the codebase has no consistent patterns.

The fix isn’t a better tech stack. It’s team alignment on how you’ll use the tools you’ve chosen. It’s code reviews that enforce standards. It’s consistent refactoring before debt piles up. It’s leadership that protects time for practices, not just features.

Your brand new codebase can stay fresh and maintainable. But only if you treat practices as seriously as tool selection.


References

¹ Tailwind CSS @apply Anti-Pattern: Adam Wathan (Tailwind creator) discourages overuse of @apply. See Tailwind CSS Style Guide which recommends against using @apply for every component and instead composing utility classes directly in HTML/templates.

² Large Component Maintainability Issues: Kent C. Dodds covers this extensively in When to Break Up a Component Into Multiple Components. The React community consensus is that components exceeding 300-500 lines become difficult to test, maintain, and reuse.