Featurize your app

By Andreas Amsenius on October 26, 2023

We should organise the source code of any non-trivial application by feature. The code gets easier to navigate and understand. It encourages to keep iterating and refactoring. Developers will face more code relevant to their problem, decreasing cognitive load. It can help to make things simpler and allow teams to keep moving fast. It can scale with an organisation, as individuals or teams specialise within features.

Background

I have struggled with complexity when developing user facing software. How to keep things simple as a code base grow? After working with more projects as a consultant I’ve found that this is a common problem. When joining a new project, I often feel overwhelmed by incidental complexity. Code bases separated into technology layers are especially painful. My mind goes: It shouldn’t be this hard?!

We lack practical advice on software architecture for modern application development. There are many concepts and technology which can help with pieces of the puzzle. But I haven’t found useful advice on how to structure whole applications as they scale.

I’ve had the opportunity to explore approaches in real projects. In recent years, things started falling into place. I gained some insights after developing an application for several years. That hunch turned into a concept during a big refactoring at Joint Academy. Then, together with the team at Elsa Science we took that concept to the next step. We encountered new challenges on the way of restructuring mobile and web apps. The solutions turned into patterns. I was able to reuse them with success in a separate greenfield project that I worked with earlier this year.

These ideas assumes projects focused on delivering value towards end-users. Where developers spend most of their time working on tasks that involve one or few “features”.

So, what is a feature?

We can define it as something like a functionality domain. A cohesive unit of user functionality. Such as, all the specific code that makes up one screen. Or set of screens, if they together form some interaction / use case. As long as cohesion is high, a feature can implement several related use cases.

A feature should live in its own directory and expose an API — the way it’s integrated into the app. It maps well to navigation routes (like page/screen or shared modal). A single route component might be the primary export of some feature. The feature hides complexity and implementation details within. This includes all the feature specific code:

  • UI components and styles
  • Types and data structures
  • Remote API calls or GraphQL queries/mutations
  • App state and business logic

Every feature needs to have a name. We should try to find names which make sense for engineers and other stakeholders. Establish a shared language by using these names everywhere. Note that the name does not define a feature, the API does. Having perfect feature names is not a meaningful goal.

Feature examples

Features rather vary in size and complexity. Here follows some examples from actual projects to illustrate this.

Typical app features

  • login / signup — Account creation / authentication screens, possibly combined.
  • onboarding — Flow of pages, introducing the service and possibly taking input from the user.
  • home — Start page when logged in, implementing or linking to the most important use cases.
  • profile / settings — User profile and app settings functionality, possibly separated.

Mobile shopping app

  • shopping-cart — Screen presenting the shopping cart and edit functionality. Includes the checkout screens.
  • video — Full screen video player

Medtech self reporting mobile app

  • medication
    • Daily medication self reporting screen
    • Search and add medication, a shared modal flow of screens.
    • Edit medication, screens included under settings.
  • reports - Flow presenting recurring multi-page visualizations of self reported data.

B2B health clinic web app

  • billing — Page presenting a table of patients for insurance claims. Includes a patient billing detail modal.
  • patient — Page presenting stats and graphs of reported patient symptoms.
  • time-entry-modal — Shared modal, CRUD form managing time spent on patient treatment. Used in both billing and patient features.

Features need to evolve

By modelling our features, we can achieve high cohesion and low coupling. The complexity increase as more and more things get exported from a feature. This may indicate the need of refactoring.

The definition of our features must change with the requirements. Details inside a feature are less important than the big picture. What does the interface look like? How do features interact and depend on each other? Do we need to introduce a new feature or split up an existing?

Feature modelling is never done. What makes sense now may not make sense later. Build for what we know today and what we can say with enough certainty about the near future. Sometimes it’s hard to draw the line and sometimes we get it wrong. That’s ok because it should be easy to change. Ongoing discussion and communication is essential.

What about common code?

Code shared between features is still important, but we need to consider what to put there and why. Developers are more reluctant to change or question the technical design of shared code. What will I break? How do I test the other use cases? Sometimes we end up with nested layers of confusing components because we didn’t dare to change the base. Or components become huge with a lot of rarely used options. By sharing code we accept the increased maintenance cost and pain if it turns out the abstraction breaks down.

DRY encourages us to avoid repeating the same concept/concern. This is different from copy/pasting code. Understand and learn about a potentially shared concept by duplicating code first. It can result in common reusable abstractions that survive longer. It also allows within a feature to be quick and dirty when needed. That’s ok, it’s easy to clean up. Local complexity is much preferred over global complexity. Feature code should change more and more often than common code. Don’t sacrifice agility to avoid duplicating code.

The devil of over-engineering is especially present around shared code. A piece of shared code may support several use cases, but that doesn’t make it a library. Premature generalisation is a problem.

Other benefits of organising code by feature

  • By looking at the path of a module, we can determine what feature it belongs to, or if it’s shared.
  • By exploring a feature folder, we can get an overview and a sense of its size or complexity. Refactoring needs are easier to align with product requirements.
  • Code review is easier since it’s clear what code has changed within vs outside of features.
  • Test case and coverage metrics. Such number become more useful as decision tools when reported per feature.
  • Features are best friends with micro frontends

Different kinds of software

My deepest area of expertise is within frontend development using React. I have arrived at these ideas after working with such projects. But I suggest that this post is about application development in general. Challenges of modern user facing software are similar. I expect to find similar benefits when developing applications using different tools. A future post might explore technology specific patterns.

User serving backend software is also related. Though the constraints are different I believe the ideas apply. I have some experience that indicates benefits and synergy effects. Such as having common feature names throughout the system. I hope this will be subject for further exploration.