Vertical Software Development

Mario Bittencourt
SSENSE-TECH
Published in
10 min readFeb 11, 2022

The Agile methodology is probably commonplace nowadays, and with it, the notion of an incremental approach is supposed to be known and leveraged by the development community. Despite this, while talking to developers I still find a disconnect between its theory and its application in the practice of day-to-day development.

I believe that this disconnect is in part due to the way we architect our solutions in layers and how, perhaps unconsciously, we tend to use the same layered approach when creating our user stories and ultimately the associated code.

In this article, I will briefly cover the benefits that Agile is meant to bring and how at SSENSE we try to leverage a layered architecture (Hexagonal, Clean, Onion, etc.), while still having the gains associated with an incremental approach.

The Agile Approach

In 2001 the Agile manifesto was released, and with it a set of principles aiming for a potentially better approach to software development. From its principles, I would like to focus on Welcoming Change and Delivering Software Frequently.

Together these two principles are usually translated into breaking the development of a given software into smaller pieces that can provide enhanced functionality and be delivered continually in a more refined way. The reasoning is simple, since no one knows exactly what is expected, it is better to implement small portions of it, see what we learn by doing so, ask more questions, and adjust/enhance its functionality in the next release.

If we look at Scrum, one of the most common Agile frameworks, we see there are similar principles, such as the iterative nature (inspect and adapt) and the definition of a list of items corresponding to what will be developed and delivered based on a given priority.

A common iteration, a sprint, will consist of a list of ordered items normally presented in the form of user stories: “As a customer I should ….”.

Let’s see how this translates into the development process when we take a layered architecture.

Traditional Approach with a Layered Architecture

Figure 1. Layered architecture and common components found in each layer.

In this example we are using Domain-Driven Design with an API backed by serverless (AWS Lambda) as illustrated in Figure 1, but you can replace them with the ones found in other architecture styles and integration patterns, such as Hexagonal or Clean Architecture.

A traditional path taken by developers is to focus on elements from each layer to guide both how the user stories are created and, consequently, implemented.

This would mean focusing on developing the Repository that will persist and retrieve the Entity, or trying to write the entire expected behavior of the Entity and its Value Objects.

Figure 2 illustrates a common case, where no progress is made on the Domain before the entire Repository functionality is implemented.

Figure 2. A traditional approach focuses on one layer at a time.

This horizontal approach has some negative aspects associated with it:

  • As a feature is only usable when it can be exposed to its clients, if the UI layer is not connected to other layers, then the value provided by delivering a given component is reduced.
  • New knowledge obtained as you develop another component from an upper layer may require you to change the previously developed, yet unused, component.

Now let’s take a look at the vertical approach.

Vertical Development

In the vertical development approach, you look at the user stories and implement the minimum necessary in each one of the layers and components to achieve the goal.

Figure 3. The vertical approach implements parts from each layer needed for a given feature.

In this example, instead of focusing on finishing each layer one at a time, you develop the parts that are needed to achieve success. Then, iteratively, you start expanding on the functionality, either by implementing non-principal scenarios, replacing mocks, or refactoring code from a given layer into a separate one.

Figure 4. Each iteration implements just the minimum code to deliver a given feature.

This approach has the following benefits:

  • Requires you to write only the code you need
  • Makes it easier to write tests associated with only the code developed
  • Aligns naturally with smaller/more focused code reviews
  • Reduces potential impedance mismatch between the layers
  • Makes your stand-up more efficient

Let’s look at how each one works together to lead to better results:

  • Requires you to write only the code you need

Traditionally you would try to develop each layer completely. This means you would aim to write the code to handle all success and failure paths, all functions associated with a certain entity, and its persistence.

This typically requires you to try and anticipate future problems or to create abstractions solely based on single-use.

In the vertical approach, you would choose to develop just the success path and, intentionally, defer the implementation of the real persistence medium, opting for a mock or a simplified version.

Inherently, this means less code to produce, which will be delivered sooner and cover a specific aspect of the whole use case.

  • Makes it easier to write tests associated with only the code developed

We should all know about the benefits of Test Driven Development (TDD) by now. It should not come as a surprise that it’s hard to follow it by breaking the habit of writing the code first and then the tests.

With a smaller, more focused approach, I have seen this difficulty diminish as you are working on a very narrow subset containing code that will later be split into separate functions/classes.

  • Aligns naturally with smaller/more focused code reviews

There is a known study that makes a correlation between the size of the code under review and the capacity to find issues with it. It is evident by that study that we should aim at having (or reviewing) small PRs, but that isn’t what tends to happen with the traditional approach.

Figure 5. The number of defects found per size of the code under review. Source SmartBear.

With less code written to satisfy a portion of the use case, you are naturally going to deliver something that will be smaller and, due to its focus, easier to understand by the reviewer.

Because you are delivering something more focused sooner, the reviewer can assess and provide feedback that can help you with the next iteration. No more working for 3 days only to receive feedback that essentially invalidates 80% of what you have done!

  • Reduces potential impedance mismatch between the layers

In the traditional approach, it is not uncommon to work on one component, for example, the persistence, before working on the UI layer that will receive the request and send back the response.

While perfectly valid, chances are as you develop the code and start to ask more questions, your understanding of the problem and therefore the solution evolves. All of a sudden the model you created is no longer suited or the behaviors have changed.

This means that the piece of code that you had created will have to change even before it has been used. And depending on the changes there is a ripple effect in multiple layers.

By using a vertical approach, because you only wrote the minimum code and have reduced the number of layers at the beginning, you have fewer moving parts that would require changes based on the new knowledge or peer review feedback.

  • Makes your stand-up more efficient

I don’t know about you, but I’ve had my share of stand-ups where there seems to be an abuse of the present continuous “I’m working on this…”.

It seems that it isn’t clear, even for the developer, what the scope is. It may even be bigger than they can grasp. So, providing a more precise definition of “what I am doing today” and any associated blockers could be hard to define.

In the vertical approach, because of the narrower focus, the scope can be more easily understood. For example, you are not trying to implement the entire Customer Repository but instead just the happy path of the insert functionality, alongside its counterparts in the other layers.

As a developer, I will be in a better position to provide my feedback during the stand-up and ask for help later on.

This all seems great and simple until we are faced with the task of actually applying this approach in our own context. A common reaction would be to just give up and flag your story as one that cannot be vertically sliced. This is probably because we have been doing things a certain way for so long that any other way seems inapplicable.

To avoid this being the case, let’s look at a step-by-step approach using a fictitious example.

Step by Step

This example provides one context for this approach. It is contrived by design so we can dissect the steps in a manageable way.

Say you have to deliver functionality that will allow a customer to place an online order. As part of the acceptance criteria you know that you can only accept up to 10 items per order and that you should not allow an order to be placed while another order for the same customer is being processed.

Step 1 — Identify principal and secondary scenarios

It is not always going to be the case, but more often than not we are interested in the happy path first, so it will be our principal scenario.

All other scenarios, usually associated with some form of failure to comply with this happy path, are secondary ones.

In our example, the principal scenario will be the one where the order is well-formed (less than 10 items) and there is no other order from the same customer being processed.

Secondary ones could be:

  • There are more than 10 items
  • There is another order from the same customer being processed

Step 2 — Develop the principal scenario with as few abstractions as possible

In our principal scenario we will receive the place order request with its payload and persist the order in the database.

If you are following a layered architecture such as the Hexagonal one, you could end up with the following structure.

Figure 6. A potential list of components after a given feature is fully implemented.

Instead of trying to do it all in a single iteration, in this step, you can do it all in the UI layer.

As you can see I have even chosen to save it in the simplest way possible. An alternative approach would skip the persistence altogether but in this contrived example it seems reasonable.

Step 3 — Work on the abstractions

Now that you have the basic working and more clarity — you may have asked some questions that now increased your knowledge of the domain — it is time to refactor/rewrite some code to push it towards the proper abstractions.

This means, for example, actually defining the command/handlers and creating your domain models, which in this form look more like simple data structures.

Note that, although not shown here, we are expected to start adding tests for some of those abstractions and, depending on your technology, leveraging mocks to replace dependencies such as your persistence layer.

One key aspect, not specific to the vertical approach, is to consider deferring the data model as much as possible so you can better understand and even pick a suitable technology as late as possible; RDBMS, Document, Graph, Key-value, etc.

In the previous code example you can see that we are simply adding the JSON version of the domain model without actually worrying about the actual access patterns and relationships.

Step 4 — Work on the secondary scenarios

Because you have a working example and the minimum abstraction in place, you can move forward. At this point, your actual use case and context will dictate. But for the purpose of our example I would add the item validation first as it seems the more common scenario of the two.

Because we are adopting a Domain-Driven approach, this validation will take place at the domain level. This means we will now start adding the proper domain logic directly to the Order entity or its properties in the form of Value Objects.

As the domain model evolves, you will have the opportunity to revisit the persistence and update the corresponding infrastructure implementation.

The tests would follow suit, being enhanced to take note of its new scenarios and components.

This step can spawn others, one for each alternative scenario or even an additional aspect. For example, you may propose to add basic validation of the payload at the UI level, leveraging some OpenAPI spec validation.

In practice, each one of the aforementioned steps can be broken down into one or more pull requests. Depending on the complexity, especially after step 2, you can even have more than one developer working on different aspects of the same step.

Conclusion

While there is no “one size fits all” approach to software development, I believe the vertical approach presented in this article is a good fit for most scenarios. It nicely marries the principles of Agile with the practice of code development.

Adopting it is not a complex endeavor but requires patience and discipline as it forces you to look at how you can organize your work differently.

If you manage to apply it, expect smaller pull requests and fewer rewrites due to integration issues, at least within your domain. You can thank me later :)

Editorial reviews by Catherine Heim & Pablo Martinez.

Want to work with us? Click here to see all open positions at SSENSE!

--

--