Maintainability with Colocation

In the continually changing world of software development, one fundamental challenge remains constant: ensuring the long-term maintainability of our codebases. As projects grow in complexity and scale, maintaining code can become an uphill battle, leading to higher maintenance costs, longer development cycles, and an increased risk of introducing bugs and vulnerabilities.

In the JavaScript ecosystem, where rapid development and requirement changes are the norm, maintaining a codebase can often be challenging. This is where the concept of "colocation" comes into play, offering a solution that can significantly improve the maintainability of your codebase.

Explore how colocation can simplify code maintenance and enhance team efficiency in software development

What is Colocation?

Colocation, in the context of software development, refers to the practice of grouping related pieces of code together within your project's directory structure. While it may sound simple, its implications are profound.

Consider the following structure:

By the looks of it, you can tell that it’s most likely a typical React project. We are “separating our concerns” by what each thing is.

What if instead we grouped our building blocks based on what they do? Then our directory structure could look something like this:

Let’s explore why that might be a good idea.

Technology Agnosticism

  • Focus on Features, Not Technology: By organizing your codebase into feature-specific directories, you can think and focus on implementing features without being preoccupied with the underlying technology or framework. This separation allows developers to concentrate on the functionality they are building.
  • Efficient Navigation: This structure makes it easier to navigate the project and find what you need. Each directory encapsulates all related building blocks (components, hooks, providers, etc.), reducing the time and effort required to locate specific functionality. You don't have to traverse through the entire project to find relevant code.
  • Ease of Changing Specific Features: When a change is needed for a specific feature, you can locate all the relevant files within the corresponding directory. This proximity of related files simplifies the modification and testing of specific features without affecting unrelated parts of the project, making it easier to maintain and update individual components.

Scalability

  • Effortless Addition of New Features: When you need to add new features to your project, the process is straightforward. You can create a new directory with the feature's name and place all the relevant building blocks within that directory. This modular approach ensures that new features are neatly organized and isolated from existing code, simplifying the development and maintenance of the project as it grows. This can be further enhanced by coming up with clear conventions for what each feature should contain and rolling out your own CLI for feature generation.
  • Efficient Code Removal and Refactoring: Consider a "Profile" component that relies on a "getFullName" utility function to display users’s full names. In a non-colocated structure, if the "Profile" component is deleted or replaced, there's a risk that the "getFullName" function might be left behind, cluttering the codebase. In a colocated setup, it's easier to recognize such unused code while working on the component, reducing the likelihood of unintentional code retention and promoting a cleaner and more efficient codebase.
  • Flexible Grouping of Features: If you find that your project has numerous feature directories and needs further organization, you can always group multiple features under another directory. This hierarchy allows for logical categorization, making it easier to manage and navigate a large number of feature directories.
  • Team Scalability: As your team expands, the feature-based structure helps a lot with the onboarding of new team members. They can quickly get familiar with a specific feature or section of the project without having to navigate through the entire codebase. This targeted approach enhances the team's agility and makes it easier to delegate responsibilities.

Technical Overview

Let’s break down some of the most common building blocks of web applications and why it makes sense to colocate them together.

  • Components: Colocating components ensures that all the visual elements, user interfaces, and their corresponding logic are placed in a single directory or file. This simplifies development and maintenance, as it allows us to work on the complete functionality of a specific component in one place, enhancing code clarity and reducing context-switching.
  • Styles: Placing styles alongside components promotes a direct relationship between visual design and functionality. Clocating styles helps maintain consistency and allows for quicker updates to the visual elements, ensuring that the user interface remains cohesive and responsive. That is one of the main reasons many developers, myself included, love working with Tailwind. It puts your styles as close as possible to related UI elements.
  • Business Logic: Colocating business logic with the components it serves ensures that all the functionality needed for a particular feature or component is encapsulated in the same location. This approach simplifies code management and reduces the risk of logic discrepancies or bugs arising from code fragmentation.
  • Tests: The same concept applies nicely with unit tests as well. Colocating tests with components, styles, and business logic encourages a more comprehensive and systematic approach to quality assurance. By having tests in the same directory as the code they assess, we can easily run, modify, and maintain tests, helping to guarantee the reliability and correctness of the specific feature.

Exceptions

While rules generally guide our decisions, exceptions can always be found. Let's explore instances where colocation may not be the most suitable approach.

  • Generic Building Blocks: Reusable UI components and utility functions that are free from domain-specific logic can be exceptions to colocation. Placing them in separate directories can make them readily available for use across the project, improving code reuse and modularity.
  • Global Configuration: Global configuration may not need to be colocated with components or business logic. Having this information in a dedicated configuration directory can be handy for managing certain parameters throughout the whole project.
  • Code-Generated REST/GraphQL Clients: Code-generated clients, often based on predefined schemas, are typically used globally and can be placed outside of the standard colocation structure. Keeping them separate acknowledges their unique nature and makes them accessible throughout the project.
  • End-to-End Tests (E2E): E2E tests should usually be kept separate from the implementation details of the application. They are designed to treat the app as a "black box," and isolating them in their own directory maintains the integrity of these tests while keeping them independent of the internal project structure.

The principles of colocation in software development stand as a fundamental guideline for creating well-structured and maintainable projects. They are universally applicable, regardless of the technology you’re using or the platform you’re building for.

This open-source project serves as a practical illustration of the concepts we've discussed in this post.

In conclusion, colocation proves to be a valuable strategy for simplifying code maintenance, fostering team efficiency, and adapting to the ever-evolving demands of software development. As we continue to innovate and face new challenges, the principles of colocation provide a solid foundation for building maintainable, scalable, and resilient codebases.

As a closing remark, let's consider this insightful statement from Dan Abramov regarding colocation: "Things that change together should be located as close as reasonable”.