Home / Blog / Engineering
Engineering

The Evolution of Folder Structure in Software Engineering

How folder structure evolved from simple MVC to Clean Architecture and Microservices — and why it reflects the engineering mindset of each era.

Yudi Nugraha
May 2, 2026
12 min read

In software engineering, change doesn't only happen at the language or framework level. One thing that quietly evolves alongside everything else is how we structure our project folders.

Many junior developers treat folder structure as just a matter of tidiness. But folder structure actually reflects the engineering mindset of its era.

The more mature our understanding of software engineering, the more deliberately we organize our code.

---

Evolution Timeline (1990–2026)

Before diving into each approach, it helps to see the big picture — how these changes actually unfolded over time.

PeriodApproachWhat Drove the Change
1990–1999No convention (flat files)The web was brand new — the only goal was "make it work"
2000–2004MVC enters the webStruts (2000), Spring Framework (2003), Martin Fowler's book (2002)
2005–2009MVC becomes the standardRuby on Rails (2005), Django (2005) — "convention over configuration"
2010–2014Layered + DDD goes mainstreamNode.js/Express, Domain-Driven Design adopted widely, microservices begin circulating
2012–2016Clean Architecture & MicroservicesUncle Bob introduces Clean Architecture (2012), Docker (2013), Netflix & Amazon pioneer microservices
2016–2019Feature-Based & Container eraKubernetes widespread (2016–2018), NestJS (2017), serverless functions take off
2020–2022Modular Monolith returnsMicroservices deemed too complex for many teams, monorepo tools (Nx, Turborepo) rise in popularity
2023–2026Plurality & context-firstNo single best architecture — the right choice depends on scale, team, and iteration speed
Each shift wasn't driven by trends. Every era emerged because real problems couldn't be solved by the previous approach.

---

The Beginning: When Applications Were Simple

In the early days of application development, system requirements were relatively modest:

  • few features
  • small team or solo developer
  • simple deployment
  • infrequent changes
  • Naturally, folder structures were kept simple.

    /controllers
    /models
    /views
    

    This layout was popularized by the MVC (Model-View-Controller) pattern.

    Files were grouped by type — all controllers in one place, all models in another.

    Strengths

  • easy for beginners to understand
  • quick to set up
  • well-suited for small applications
  • Weaknesses

    As the application grows:

  • controllers multiply and become hard to navigate
  • models become difficult to locate
  • a single feature is scattered across multiple folders
  • maintenance becomes increasingly painful
  • When Simple MVC Makes Sense

    This structure works well for:

  • Company profile / landing page — a static website with a few forms and pages
  • Simple internal tools — employee attendance dashboards, leave request forms
  • Early prototype / MVP — validating an idea in 1–2 weeks, scalability not yet a concern
  • Solo side projects — personal blogs, portfolios, small CLI tools
  • Real-world example: a new startup building a waitlist registration page with email notification. Simple MVC is more than enough.

    ---

    The Growth Phase: Layered Architecture

    As applications grew more complex, the layer-based approach emerged:

    /controllers
    /services
    /repositories
    /models
    

    Business logic moved into /services, and database access was isolated in /repositories.

    This was an important step forward — developers started internalizing concepts like:

  • separation of concerns
  • single responsibility
  • reusable business logic
  • The Positive Impact

    Code became more structured, and each layer had a clearer responsibility.

    However, as systems scaled further, a new problem surfaced.

    To understand the "Order" feature, a developer now had to open:

  • the order controller
  • the order service
  • the order repository
  • the order model
  • Everything was still scattered — just more neatly organized.

    When Layered Architecture Makes Sense

    This approach fits well when:

  • Building a REST API with 5–15 endpoints — a backend for a mobile or web app that's beginning to grow
  • Simple booking systems — restaurant reservations, sports facility booking, doctor scheduling
  • Early-stage e-commerce — an online store with basic product, cart, and order features
  • Teams of 2–5 developers — enough to need shared conventions, but not yet complex enough to warrant full module separation
  • Real-world example: a 3-developer team building an API for a retail point-of-sale app. Features include products, transactions, and reports. Layered architecture makes task delegation easier without adding unnecessary overhead.

    ---

    The Modern Era: Feature-Based Structure

    As teams and products continued to grow, the focus shifted from "file type" to business feature.

    A structure like this emerged:

    /modules
      /users
      /orders
      /payments
    

    And inside each module:

    /orders
      order.controller.ts
      order.service.ts
      order.repository.ts
      order.model.ts
      order.dto.ts
      order.spec.ts
    

    Why This Became Preferred

    Because developers work around features, not file types.

    When asked to work on the Order Module, everything needed — including tests — lives in one place.

    Best Fit For

  • fast-growing startups
  • medium to large teams (5–20 developers)
  • applications with a growing number of features
  • When Feature-Based Makes Sense

  • SaaS products — HR platforms, project management tools, or CRMs with dozens of features
  • Multi-tenant applications — a single backend serving many clients with different configurations
  • Series A startups and beyond — products that have found product-market fit and are actively scaling
  • NestJS or Spring Boot backends — these frameworks are explicitly designed around modular organization
  • Real-world example: a fintech startup building a lending app with modules for users, loans, repayments, notifications, and reports. Each squad can own their module independently without constant merge conflicts.

    ---

    The Maturity Stage: Clean Architecture

    As software engineering matured, deeper requirements emerged:

  • code must be easily testable
  • no tight coupling to any specific framework
  • business domain must be protected
  • scalable over the long term
  • Clean Architecture addresses all of these:

    /src
      /Domain
        /Entities
        /ValueObjects
        /Repositories (interfaces)
      /Application
        /UseCases
        /DTOs
      /Infrastructure
        /Database
        /ExternalAPIs
      /Presentation
        /Controllers
        /Middlewares
    

    The Philosophy

    Dependencies must point inward — toward the business rules — never outward.

    This means:

  • the database can be swapped (PostgreSQL → MongoDB) without touching the domain
  • the framework can be replaced (Express → Fastify) without changing use cases
  • the UI can be redesigned without affecting business logic
  • the domain layer remains stable, safe, and unit-testable
  • When Clean Architecture Makes Sense

    This is not for every project. It's the right choice when:

  • Fintech or banking systems — complex domain rules, strict regulations, mandatory audit trails
  • Healthcare platforms — patient records, medical history, integration with medical devices
  • Enterprise ERPs — business logic that frequently changes in response to company policy
  • Platforms built for the long haul — systems expected to be maintained and evolved over 5–10 years
  • Real-world example: a digital insurance platform subject to strict financial regulation. The domain rules for premium calculation and claims processing are highly complex. Clean Architecture lets the team swap payment providers and update business rules without risking breakage elsewhere.

    ---

    Enter Microservices

    When a single application becomes too large to manage, organizations began decomposing their systems into independent services:

    /user-service
    /payment-service
    /product-service
    /notification-service
    /api-gateway
    

    Each service has its own internal structure, its own database, and its own deployment pipeline.

    This is no longer just about folder structure — it's an evolution toward system architecture.

    When Microservices Make Sense

    Microservices are not a default solution. They solve a specific class of scaling problems:

  • Large marketplace platforms — where checkout, search, and chat need to scale independently from one another
  • Super-apps — combining ride-hailing, food delivery, payments, and logistics in one ecosystem
  • Large engineering organizations — 50+ developers where each squad needs full ownership over their service
  • Systems with uneven traffic — the notification service may need 10x more resources during a flash sale, while billing does not
  • Real-world example: an e-learning platform with millions of users. The video-streaming service requires dedicated CDN infrastructure, while the quiz service runs on a small server. Microservices allow both to be scaled independently based on their own demands.

    ---

    Between Monolith and Microservices: Modular Monolith

    When Clean Architecture is already in place but the team isn't ready for the operational complexity of microservices, there's one option that's often skipped: Modular Monolith.

    A Modular Monolith is still a single deployment unit — like any monolith — but module boundaries are enforced explicitly by tooling, not just folder conventions.

    /modules
      /orders
        /public           ← the only part other modules may access
          index.ts
        /internal         ← hidden from the outside world
          service.ts
          repository.ts
      /notifications
        /public
          index.ts
        /internal
          service.ts
    

    What sets it apart from Feature-Based:

  • In Feature-Based, the orders module can directly import any class from the notifications module
  • In Modular Monolith, orders can only access what notifications explicitly exposes through its public API
  • This boundary is enforced by tooling — ESLint rules (Node.js), the internal keyword (C#), or import-linter (Python) — not just team convention
  • Why This Matters

    Modular Monolith serves as a very useful bridge:

  • Single deployment — none of the distributed system complexity: no network failures, no retries, no eventual consistency
  • Clear, tested boundaries — if the system ever needs to migrate to microservices, module boundaries are already well-defined and proven
  • Better developer experience — no contract testing, no network calls between modules, but code is still rigorously organized
  • When Modular Monolith Is the Right Fit

  • Teams that have outgrown Feature-Based but aren't ready for microservices complexity
  • Systems likely to split into microservices in the future — Modular Monolith is the best preparation
  • Organizations with 10–30 developers who still want a single deployment unit
  • Systems with high data consistency requirements that don't fit well with distributed transactions
  • Real-world example: a Series B fintech startup with 15 developers. The system has outgrown Feature-Based but doesn't yet need microservices infrastructure. Modular Monolith lets them maintain iteration speed while keeping domain boundaries clean and ready for future splitting.

    ---

    Can Every Architecture Be Tested

    Yes — all of them can. But the quality and ease of testing vary significantly.

    ArchitectureUnit TestIntegration TestMocking Ease
    Simple MVCHard — logic and I/O are mixed in the controllerPossible, but requires a real databaseMust mock many things at once
    LayeredMedium — service layer is testable, but coupling often lingersEasier per layerRepository can be mocked
    Feature-BasedEasy — test files live inside the modulePer-module, isolatedDependency injection helps
    Clean ArchitectureVery easy — Domain Entity is a pure object with zero dependenciesOnly at the Infrastructure layerPorts/interfaces are designed to be mocked
    Modular MonolithEasy — same as Clean Architecture per moduleModule contract test via public API onlyMock only through the module's public interface
    MicroservicesEasy per serviceRequires contract testing between servicesEach service is independent, but external services must be mocked

    Key Insights

    Simple MVC — testing is possible but painful. It usually ends up as heavy integration tests because business logic cannot be separated from HTTP and the database.

    Clean Architecture — the only architecture explicitly designed with testability as a primary goal. The domain layer can be tested 100% without a database, without HTTP, without any framework.

    Microservices — introduces a new challenge: contract testing. Unit testing per service is straightforward, but ensuring two services can actually communicate correctly requires additional tooling such as PactNet (.NET) or Pact.js (Node.js).

    If testing is the top priority, the architectural choice narrows to Clean Architecture — and the larger the system, the greater the benefit.

    ---

    What's Really Evolving

    It's not the folders themselves.

    What evolves is our understanding of software complexity.

    Folders are just a representation of engineering thinking.

    From simply tidying up files, to managing change, teams, risk, and business scale.

    ---

    Is Modern Structure Always Better

    No.

    For small projects, simpler structures are often more effective.

    A small internal tool:

    /controllers
    /models
    

    Is perfectly sufficient.

    Forcing enterprise architecture onto a small project leads to overengineering — wasting time on infrastructure setup that isn't needed, and slowing down iteration at a stage that should be fast.

    Quick Reference Guide

    Team SizeComplexityRecommended Architecture
    1–2 peopleLowSimple MVC
    2–5 peopleMediumLayered
    5–20 peopleHighFeature-Based
    10–30 peopleVery HighClean Architecture or Modular Monolith
    20–50 peopleEnterprise (single deployment)Modular Monolith
    50+ peopleEnterprise (distributed)Microservices
    ---

    The Right Principle

    Choose a folder structure based on:

  • team size
  • business complexity
  • project maturity
  • rate of feature change
  • the skill level of the developers maintaining it
  • Not because of internet trends.

    ---

    Conclusion

    Yes, folder structure evolves alongside software engineering itself.

    The journey looks roughly like this:

    > Simple MVC → Layered → Feature-Based → Clean Architecture → Modular Monolith → Microservices

    Each stage emerged to solve the problems of the stage before it.

    Folder structure is not decoration.

    It is the footprint of how humans have learned to build increasingly complex software.

    ---

    Closing Thoughts

    A mature developer doesn't ask:

    > "What is the best folder structure?"

    They ask:

    > "What structure best fits the problem I'm solving right now?"

    That is genuine engineering thinking.

    Tags

    Software EngineeringArchitectureBest Practices
    Y

    Yudi Nugraha

    Software Engineer | Builder

    More Articles

    Explore more articles on similar topics

    View All Articles