Why I Recommend a Feature-Driven Approach to Software Design

Last updated Apr 23rd, 2021
Features represent the essential complexity of software design. It's the complexity that can't be avoided. Everything else — the languages, tools, patterns, etc — are a form of accidental complexity. Therefore, to write the simplest possible code (regardless of which side of the stack we're on), we should take a feature-driven approach. It influences the way we structure our projects, write our tests, and package modules, and design features.

Introduction

Here are a few pressing questions:

  • What is the single-most important concept you can tell me about software design?
  • How do you structure a (frontend or backend) so that it's testable, flexible, and maintainable?
  • What am I supposed to test against in my tests? Classes? Methods? React components? How can I write tests that actually give me confidence in my code?

At first glance, these questions might appear to be a little bit unrelated. General software design advice, project structure, testing best practices. Hard to see the link, right?

After having studied software design for a few years, the answer to each of these questions — and nearly all questions about how to write testable, flexible, and maintainable software, is to focus on the features.

That is, I'm telling you to be feature-driven.

Features are the only essential complexity

For "Part II — Humans & Code" of solidbook.io, I had a chance to read Out of the Tar Pit by Ben Moseley.

In it, he discusses the ideas of essential complexity and accidental complexity.

Complexity

Essential complexity

Software, in its simplest form, is a way to tame complexity in the world. We do this by developing use cases (or features, you may call it) that cut through entropy and perform something useful for us.

If my goal is to "make friends in a new city", I can divide the application that helps me do that up into use cases/features. Three features/use-cases may be:

  • Sign up
  • Edit profile
  • Get friend suggestions

The essential complexity involved in solving the problem of "making friends in a new city" is to first identify all of the features - then, to merely implement them.

Once all the features/use cases/vertical slices are implemented, we're done.

Accidental complexity

However, since we live in the real-world, we have to write code to accomplish this.

And in most mainstream programming languages, the concepts of state and sequence exist.

What's wrong with state and sequence?

Well, according to Ben Moseley, state and sequence are the two leading causes of accidental complexity.

  • State — "Have you tried turning it off and on again?" Has anyone ever said that to you before? This is what happens when a system ends up in an illegal state. And maintaining state is hard. We have to keep track of variables, instances, etc.
  • Sequence — "First we do this, and then we need to do this, but only if this happens". Most of us write in languages that support procedures and conditionals. When we have to ensure that things happen in a particular order, we're also introducing surface area for complexity.

Accidental complexity is when the complexity we're faced with is not actually related to the complexity of the problem (like the essential ones — the features). Instead, accidental complexity is related to the way we solve problems.

That means that languages or paradigms that contain state and sequence inadvertently introduce accidental complexity.

Okay, can we write code without state and sequence?

It turns out that we can.

Mitigating accidental complexity with functional programming

This is where (purely) functional programming shines. Scott Wlaschin's Domain-Modeling Made Functional book depicts an approach to get as close to the essential complexity as possible by modelling features/use cases (he calls them workflows) using minimal-to-no state and sequence.

For example, here's some pseudo-code for a feature that can be modelled entirely using F#'s algebraic type system.

Functional Domain Modeling

From "Domain Modeling Made Functional" by Scott Wlaschin - highly recommended reading.

This is a really thorough approach. I think it's amazing. But real pure-ish functional programming isn't for the faint of heart. This is some seriously disciplined programming. By modeling the entire domain from scratch, similar to the way we build up math equations from first principles, this is truly the way that we make illegal states unrepresentable.

However, most of us want a little bit of state and sequence. Modelling purely functional features like this can be challenging. For many, composing software this way is an entirely different way to think about programming. Even RxJs can be challenging.

https://patrickroza.com/blog/result-composition-and-error-handling/

Patrick Roza

See Patrick Roza's Result Composition and Error Handling techniques in TypeScript. I currently think that pure functional programming in TypeScript sits further on the structural side than it does on the developer experience side.

Structure vs. developer experience — FP is hard

Like most things in design, there's a push-pull between priorities.

In software design, I call it the balance between Structure and Developer Experience.

You want a structured approach to doing things, but you don't want it to hinder your productivity.

Use%20a%20Feature-Driven%20Approach%20to%20Software%20Design%207e6e084055f8468cb3d9d8db19cbd148/Ezbg2UJX0AIfpBA.jpg

Structure vs. Developer Experience - from solidbook.io

Use%20a%20Feature-Driven%20Approach%20to%20Software%20Design%207e6e084055f8468cb3d9d8db19cbd148/EzbhnOKXEAET_GB.jpg

Balancing structure vs. developer experience - from solidbook.io

Therefore, the majority of the web development industry codes in paradigms and with languages that deal with state and sequence (TypeScript, JavaScript, Python, etc).

Well, that's certainly some accidental complexity that we're taking on isn't it? Yes... and it's fine.

But that doesn't mean we don't need to keep tabs on it.

Luckily for us, OOP folks from the 90s discovered a great feature-first way to consistently, reliably, and confidently write code containing state and sequence.

It's called TDD.

Use feature-first TDD to tame (state and sequence) complexity

There's more to Test-Driven Development than just unit testing React components and classes.

The best ROI comes through figuring out a way to continue to add and change code, and ensure that the features of our application still work. This is where the majority of our efforts should lie — because the features are the essential complexity, after all.

The "feature-first way" to do TDD is to do Double Loop TDD.

  • Outer loop: Write the acceptance test (which can be written as an integration or end-to-end test); this is the essential complexity written in English.
  • Inner loop : Red-Green-Refactor your components, classes, etc until the outer loop passes.

Double Loop TDD

This isn't a new thing, I just think that we're all struggling to agree on the terminology and recognize that we're all talking about the same things (see this blog post from the Dodds).

A collection of feature-first techniques

Some of the best techniques I've found that help us align with the essence of the problem at hand and cut out the noise (tech stack, language, paradigm, etc) are as follows:

Benefits of a feature-first approach

We get a lot of benefits when we apply a "feature-first" approach.

Project structure

At the folder-level:

  • More cohesive feature folders
  • More discoverable folder structures
  • Less "cognitive load" flipping around files/folders
  • Screaming architecture (easy to remember what the system does)

Overall system simplicity

At the system level:

  • Consistent testing strategy with a high ROI (acceptance test for each feature)
  • Simpler architecture
  • Easy to keep tabs on coupling between features/vertical slices (SRP)

Vertical slices

Software craftsmanship

At the professional level

  • Decouple large projects into sets of features
  • Provide better estimates
  • Understand where new technologies fit into a vertical slice (feature)

Conclusion

DDDForum (GitHub Repo)

You can work in a feature-driven way on both front-end and back-end architectures. Take a look at the code for DDDForum, the app we build in solidbook.io.

The backend follows a feature-first project structure, organized by use cases.

DDDForum project structure

The frontend application is currently being refactored to follow a feature-first approach for the next iterations of the Client-Side Architecture Basics guide. In the next iterations, you'll learn:

  • How to craft a feature-first project architecture
  • How to consistently evolve code using TDD & Unit, Integration, and E2E tests regardless of your stack

Part II — Humans & code from solidbook.io

I'll be writing more content about this phenomenon on the blog, but if you're looking for an in-depth discussion on how to design human-friendly codebases, the Humans & Code part of solidbook.io just went out last week. We learn how to structure repos, organize things, name them, handle errors (and more) — all in a feature-first way.

Use%20a%20Feature-Driven%20Approach%20to%20Software%20Design%207e6e084055f8468cb3d9d8db19cbd148/EzbYtjuX0AA3YSu.jpg

Use%20a%20Feature-Driven%20Approach%20to%20Software%20Design%207e6e084055f8468cb3d9d8db19cbd148/EzbYvBXWQAEqPUL.jpg

Use%20a%20Feature-Driven%20Approach%20to%20Software%20Design%207e6e084055f8468cb3d9d8db19cbd148/EzbYwH4XIAM2jo0.jpg

Use%20a%20Feature-Driven%20Approach%20to%20Software%20Design%207e6e084055f8468cb3d9d8db19cbd148/EzbYws_WUAUb6q4.jpg

Check it out here.



Stay in touch!



View more in Software Design