The Analog Refactor City: Zoning Your Codebase Like an Urban Planner Before You Add One More Feature
Before you bolt one more feature onto a legacy system, treat your codebase like a city. Learn how zoning, infrastructure planning, and citizen involvement can guide safer, incremental refactors that make your software scalable, testable, and maintainable.
The Analog Refactor City: Zoning Your Codebase Like an Urban Planner Before You Add One More Feature
Software engineers and urban planners have more in common than you think.
Both inherit messy, living systems: cities with chaotic growth; codebases with decade-old hacks. Both face pressure to “just add one more thing” — a new shopping mall here, a new feature flag there — even when the underlying infrastructure is buckling.
Instead of blindly piling on more features, what if you treated your legacy codebase like a city and approached refactoring like urban planning?
In this post, we’ll explore how zoning, infrastructure, and citizen input can guide safer, incremental changes that make your system more understandable, testable, and maintainable — before you add the next skyscraper.
1. Your Codebase Is a City, Not a Puzzle
We often talk about code as if it were a static puzzle: fixed pieces to be arranged into a single correct solution. But most mature systems behave more like cities:
- They grow organically over time.
- Different “neighborhoods” were built by different people, with different standards.
- There are shortcuts, back alleys, and weird dead ends that nobody remembers building.
- People still need to live — and work — there while you improve it.
Thinking in city metaphors helps reframe refactoring:
- Districts: Feature areas, modules, services.
- Streets and highways: Function calls, message queues, APIs, data pipelines.
- Utilities: Logging, monitoring, configuration, build systems, CI/CD pipelines.
- Zoning rules: Architecture guidelines, coding standards, dependency rules.
Before adding another “building” (feature), ask: where does it belong in this city, and what infrastructure will it demand?
2. Zoning: Draw Boundaries Before You Build
Urban planners don’t start by placing individual buildings. They start with zoning: where residential, commercial, and industrial structures may go, and what’s allowed in each district.
You can do the same in your codebase.
Define districts in your system
Even in a messy legacy monolith, you can identify and label de facto districts:
- Presentation district: UI templates, controllers, API handlers.
- Domain district: Business rules, core entities, value objects.
- Infrastructure district: Database access, external APIs, messaging, file systems.
You may discover:
- Controllers talking directly to the database from random places.
- Business rules hiding inside view logic.
- Utility classes used as “mixed-use high-rises” for everything.
Instead of a Big Bang rewrite, start with zoning boundaries:
- “New business logic should live in the domain layer.”
- “Controllers can depend on domain services, not directly on the database.”
- “Infrastructure details must not leak into domain objects.”
These are lightweight rules — like zoning codes — that begin to constrain where new code can go and what it can depend on.
Zoning is about containment, not perfection
You won’t instantly clean up the city. But zoning gives you a way to:
- Contain new complexity within clear boundaries.
- Keep local changes from spilling into the entire codebase.
- Slowly reshape the layout of the system while still shipping.
Think of each boundary as a soundproof wall: changes in one district should not force every other district to be rebuilt, retested, and re-understood.
3. Urban Planning Is Interdisciplinary — Refactoring Should Be Too
Urban planning isn’t just about drawing pretty maps. It blends:
- Architecture (what should buildings look like?)
- Civil engineering (will the bridges hold up?)
- Social science (how do people actually use the city?)
- Politics and economics (what can we afford? who’s impacted?)
Refactoring your codebase works the same way:
- Architecture: What should the modular boundaries and layering be?
- Engineering: What’s technically feasible given existing dependencies and runtime constraints?
- Design & UX: How will this change affect flows, usability, and defect patterns?
- Social factors: How do your teams work? Who owns what? Where do handoffs break down?
If you design a “perfect” architecture that ignores how your developers, QA, and ops teams actually work, your zoning plan will fail. People will route around it.
Instead:
- Involve developers in deciding module boundaries and code ownership.
- Involve QA in defining testing boundaries and regression risks.
- Involve ops and SREs in figuring out deployment, observability, and failure domains.
Treat these stakeholders as citizens of your code city, not passive recipients of top-down plans.
4. Study the Street Network Before You Redesign the City
Planners don’t bulldoze highways based on vibes. They analyze traffic, land use, and pedestrian flows.
Your equivalent is the dependency graph — the “street network” of your code city.
Map your dependencies
At minimum, understand:
- Which modules depend on which other modules?
- Which services call which APIs or message topics?
- Where are the tight cyclical dependencies?
Tools can help (static analysis, architecture diagrams, call graph visualizers), but even a rough, manually-drawn map is better than intuition.
Look for:
- Highways: Core services or libraries that everything calls into.
- Choke points: Classes or modules with a huge number of dependents.
- Illegal shortcuts: Low-level modules depending on high-level ones, crossing intended boundaries.
Use the map to guide incremental change
Once you see the map, you can plan like a city engineer:
- Relieve pressure from choke points by extracting stable interfaces.
- Create ring roads (facades or anti-corruption layers) that shield messy neighborhoods.
- Close dangerous cut-throughs (forbid certain cross-layer dependencies, add lints or enforcement).
This lets you refactor the "city grid" gradually: adding new routes, deprecating old ones, and changing traffic patterns without shutting down the entire city.
5. Beware: Modern Build Systems in a Sprawling City
Modern build systems are like advanced construction machinery. They’re powerful, but they can also magnify your existing problems.
If your dependency graph is a tangled mess, adding a fast, sophisticated build tool won’t magically fix it — it’ll just make your global rebuilds faster and more painful.
The core problem isn’t that your crane is slow. It’s that every time you adjust a balcony, the zoning rules force you to recertify the entire skyline.
How tangled dependencies hurt builds
When everything depends on everything else:
- Small code changes appear to affect large portions of the system.
- Build tools conservatively rebuild massive swaths of code.
- CI times spike, making refactoring and experimentation more expensive.
Instead of blaming the build tool, address the city plan:
- Introduce clear module and layer boundaries.
- Minimize cross-district calls.
- Move towards well-defined interfaces and dependency inversion.
Only after you simplify the city layout will modern build systems deliver their real benefits: incremental builds, parallelization, and reliable caching.
6. Favor Incremental Street Work Over Heroic Bulldozers
The fantasy: shut down the city, bulldoze everything, and build a perfect, grid-aligned metropolis.
In software terms: the heroic full rewrite.
Reality:
- Full rewrites are risky, expensive, and often fail before feature parity.
- Business needs don’t pause while you redraw the map.
- You lose the embedded knowledge and weird edge cases handled in the legacy system.
Urban planners usually work around what exists:
- Add new streets.
- Rezone specific districts.
- Upgrade utilities block by block.
You can do the same with your codebase.
Patterns for incremental refactoring
- Strangler Fig pattern: Wrap old functionality behind a new interface; route some traffic to the new implementation, gradually expanding until the old code can be retired.
- Anti-corruption layer: Put a boundary around legacy modules, translating between old and new models so the rest of the system isn’t infected by legacy constraints.
- Boy Scout rule: Whenever you touch a file, leave it slightly cleaner: improve names, extract a function, add a missing test, move logic to the right "district".
Think in terms of:
“What can I safely rezone on this block today?” rather than “How do I rebuild the whole city?”
These small, low-risk moves accumulate into large, structural improvements.
7. Involve the Citizens: Shared Guidelines, Not Secret Master Plans
A city plan that lives in a single binder on a shelf is useless. Likewise, an architecture diagram no one follows is just wall art.
To make refactoring stick, involve your “citizens” and codify habits.
Make the zoning rules explicit
- Write a short architecture decision record (ADR) for key boundaries: “UI depends on domain, domain is pure, infrastructure is replaceable.”
- Create lightweight code review checklists that emphasize boundaries: "Does this new code respect the layering?" "Are we leaking infrastructure details into domain objects?"
- Add automated linters or module boundary checks where possible.
Consult the people doing the work
- Ask developers: Where do changes feel riskier than they should? These are often bad intersections or missing boundaries.
- Ask QA: Which areas are fragile or require exhaustive regression testing? These hint where zoning is failing and local changes have global impact.
- Ask ops/SRE: Which services or modules cause the most trouble in production? These are your overloaded utilities and fragile bridges.
When people see that refactoring plans align with their lived experience of the system, they’re much more willing to honor the new “zoning codes”.
8. Putting It All Together Before Adding the Next Feature
Before you add one more feature to your legacy code “city,” pause and:
-
Map the city
- Identify major districts (UI, domain, infrastructure, cross-cutting concerns).
- Visualize key dependencies and call paths.
-
Define or refine zoning
- Decide what kinds of code belong where.
- Write down simple, enforceable rules for new code.
-
Plan infrastructure improvements
- Fix the worst choke points and tangled intersections first.
- Introduce facades or anti-corruption layers around messy areas.
-
Ship features in a city-friendly way
- Place new features in appropriate districts, respecting boundaries.
- Use incremental patterns (strangler, boy scout) to improve what you touch.
-
Engage the citizens
- Align refactoring with team workflows, testing, and operations.
- Iterate based on feedback; adjust the plan as the city evolves.
Conclusion: Build a City You Can Live In
You don’t need a perfect, shining smart city. You need a place where you can build, maintain, and safely evolve software over years.
By treating your legacy codebase like a city and your refactors like urban planning, you:
- Make structural problems visible in everyday language (districts, streets, infrastructure).
- Prioritize boundaries and zoning over hero rewrites.
- Use dependency maps to guide focused, incremental improvements.
- Align technical changes with how your team actually works.
The next time someone asks, “Can we just add this one feature?”, your answer doesn’t have to be a resigned “Sure.”
Instead, you can say: “Yes — but first, let’s check the zoning map.”
That mindset shift is how you turn a fragile legacy sprawl into a resilient, livable code city.