Skip to content
>GLB_
Go back

Greenfield vs Brownfield Projects: What Actually Changes in Engineering

TL;DR — Greenfield means you start from an empty repo and your main risk is building the wrong thing. Brownfield means you inherit a running system and your main risk is breaking something nobody remembers depends on it. The skills overlap, but the default moves are opposite.

Definitions

The terms come from urban planning and construction:

In software (Wikipedia: Greenfield project, Wikipedia: Brownfield (software development)):

GreenfieldBrownfield
Starting pointEmpty repositoryExisting codebase in production
UsersNone (yet)Real users, real data, real SLAs
Main constraintUnknown requirementsExisting behavior and integrations
Main riskBuilding the wrong thingBreaking something that works
ArchitectureChosenInherited
Feedback loopSlow (no users)Fast but noisy (prod incidents)
Typical mistakeOver-engineeringBig-bang rewrite

Most real projects are a mix. A new microservice inside an old platform is greenfield code in a brownfield environment: it has to speak the existing auth, the existing message format, the existing deployment pipeline.

Greenfield: freedom is the problem

An empty repo looks like the easy case. It isn’t — it just moves the difficulty.

What you get

What goes wrong

1. Designing for scale you don’t have. Kubernetes, event sourcing, and five microservices for a product with zero users. Every abstraction you add before you have a real use case is a guess, and you will pay for each wrong guess later.

Gall’s Law applies directly: a complex system that works evolved from a simple system that worked. Start with the simplest thing that ships.

2. Unknown requirements disguised as known ones. The spec is a hypothesis. The first real users will invalidate parts of it. Optimize for cheap change, not for the design on the whiteboard.

3. Skipping the boring foundations. The things that are trivial to add on day 1 and painful on day 300:

# Day-1 checklist that's nearly free now and expensive later
- Linting + formatting enforced in CI
- Tests running on every PR
- Infrastructure as code (no click-ops)
- Structured logging + a metrics endpoint
- Reproducible local env (devcontainer / docker compose)
- Secrets out of the repo from commit #1
- Architecture Decision Records (ADRs)

ADRs are worth calling out: in a greenfield project you are the person who knows why every decision was made. In two years someone else will inherit it as brownfield. Write it down (Michael Nygard: Documenting Architecture Decisions).

Greenfield default moves

Brownfield: the system is the spec

When you inherit a running system, the code is not just an implementation of requirements. It is the requirements — including the ones nobody wrote down.

What you get

What goes wrong

1. Hidden contracts. Some downstream job parses a log line. A partner relies on a field being null instead of missing. A report depends on a sort order that was never guaranteed. This is Hyrum’s Law: with enough users, every observable behavior of your system will be depended on by somebody.

2. No tests, so no safe changes. Michael Feathers defines legacy code simply as code without tests (Working Effectively with Legacy Code, Prentice Hall, 2004). Without tests you can’t refactor; you can only edit and hope.

3. The rewrite trap. “It’s a mess, let’s start over.” The rewrite becomes a greenfield project that must reach parity with a moving target, while the old system keeps getting patched. Joel Spolsky’s classic essay on this is still accurate (Things You Should Never Do, Part I).

Brownfield default moves

Map before you touch. Dependencies, data flows, cron jobs, who calls what. Read the deploy pipeline and the runbooks before the code.

Pin current behavior with characterization tests. Not “what should it do” but “what does it do right now”:

# Characterization test: lock in existing behavior, even if it looks wrong
def test_invoice_total_rounding_current_behavior():
    # Legacy code rounds each line item, not the total.
    # Downstream reconciliation depends on this. Do not "fix" without a plan.
    assert calculate_invoice_total([10.005, 10.005]) == 20.02

Change incrementally, behind seams. Two patterns cover most cases:

             ┌──────────────┐
  client ──▶ │   facade /   │──▶ new service   (routes migrated)
             │   proxy      │──▶ legacy app    (everything else)
             └──────────────┘

Make every change reversible. Feature flags, dark launches, shadow traffic, and a tested rollback. In brownfield, rollback speed matters more than deploy speed.

Improve observability first. You can’t safely change what you can’t see. Adding metrics and structured logs to the legacy system is usually the highest-leverage first PR.

Data engineering version

The same split shows up in data platforms:

Greenfield data platformBrownfield data platform
ExampleNew lakehouse, no consumers yetMigrating a 10-year-old warehouse
SchemaDesigned upfrontDefined by existing reports and ETL
Hard partModeling for questions not asked yetParity: every dashboard must still match
Key techniqueContracts and conventions from day 1Parallel run + row/aggregate reconciliation

For brownfield migrations, the parallel run is the strangler fig for data: both pipelines run, outputs are diffed, consumers move table by table once the numbers reconcile. Tools like dbt make this tractable through tests and versioned models (dbt: Data tests, dbt: Model contracts).

How to decide: rewrite or evolve?

When you’re in a brownfield system and someone proposes a greenfield replacement, check these:

If most answers are “no”, evolve incrementally. A rewrite without those answers is a greenfield project with brownfield expectations — the worst of both.

Summary

References


Share this post:

Related Posts


Previous Post
Working with JSON in Python and pandas: json.load, JSON Lines and read_json
Next Post
Batch API Requests in Python: Throughput vs Rate Limit Trade-Off