Choosing Effective Code Layout Strategies for Software Projects

When designing and organizing code for application frameworks, the primary goal is to keep code easy to read and maintain. To achieve that, it’s imperative that the code is organized in a logical manner. Engineering teams looking at an overview of the system should be able to gather enough context without having to read a design document and go through extensive training. The mark of a good codebase is that engineers should be able to become familiar with the code in a matter of days even if it uses new and innovative constructs.

In software engineering, it’s rare to have a single optimum design, pattern, or architecture that can be applied without context. As the old saying goes, “if all you have is a hammer, everything looks like a nail”; in software engineering, if all you have is one framework, every process follows the same design. 

Software engineers often fall into the trap of picking a particular methodology of organizing code that is most familiar to them rather than giving credence to different requirements, or more importantly, the way the application will evolve in the future. I’ve personally come across multiple projects where the code layout was complex and confusing and the two most common reasons why this happens are unexpected software evolution and solution architects sticking to what they know rather than picking an optimal solution. When you consider how much more time and effort it takes to learn and apply something new, it seems natural to stick to what you already know. Solution architects have many variables to juggle so the more they can position the project in their comfort zone, the more control they can exert on it. 

The choice of code layout can make a significant impact both in the short and long term result of the project. If you ever read about a “one solution fits all” code layout strategy, I recommend taking it with a grain of salt. 

To provide a concrete example of different layout strategies, I’ll be using the Spring Framework using Spring Boot to organize a sample server project. Spring is a powerful yet un-opinionated framework that does not restrict engineers from choosing how to layout the code. Spring Boot adds a layer of opinion where the configuration is done by definition rather than in complex configuration files. I’ll discuss two common code structure strategies, namely separation by type and separation by concern, what they are, and in what case you would use them.

Sample Server Project

I decided to use a simple example to demonstrate the two methodologies. The server in this example primarily concerns two entities, Cars and Users, and tracks the relationship between them. Apart from there being a CRUD interface for the Car entity, the project also requires special endpoints to associate and disassociate Cars from Users. 

Separation by Type

This philosophy is a common one and is my preferred method of organizing server code, with a few tweaks here and there. Separating by type splits up files based on what type or kind they are. In Spring, there are many types of files:

  • Configurations: Where you keep server configurations, and singleton utilities like File Storage, Database Configurations, and Security Configurations.
  • Domain/Model/Entity: These are your ‘dumb’ entity or data files that often contain the objects you will store in the database and how they will be modeled.
  • Repositories: These are interfaces for how you fetch Models from the database.
  • Controllers: These are the endpoint interface files that represent how external clients will connect to your server.
  • Services: These contain business logic that determines how your server behaves and how it processes data.

Each type gets placed in its own unique package. The only file outside a package is the Spring Boot Application file, which serves as the entry point to the application and contains nothing except for the starter code. Here is an example of a bare-bones layout:

An example of a bare-bones layout.

When you should use it

This strategy works best when the project size is small to medium in complexity, requires fast iterations, and you have a relatively small developer team that is ideally colocated and tight-knit. 

Why you should use it

With each type of file being within its own package, it’s very easy to look at this structure and quickly find where something is. If I want to know exactly what endpoints are available for the Car entity, all I have to do is open CarController. If I want to know what calls or queries are available to me to fetch users from the database, I can open UserRepository. 

This kind of structure binds code tightly together and can allow the engineering team to agree on a certain set of guidelines that make it easier for everyone to locate code because each type of file is organized in a similar way. A simple example to demonstrate this would be to organize a service file with CRUD functions at the top with other complex business logic functions below.

Organizing method definitions

This not only makes all files look neat but allows the core team who designed the guidelines to find a relevant code block quickly. 

In the case of rapidly evolving software requirements or features that operate on multiple entities, a small engineering team can quickly agree on where to place the feature. For example, if a feature requires operations on both Cars and Users, the engineering team can say, place the feature in the CarService and put methods in the UserService to gain access to the User entity without having to directly interface with the UserRepository. 

Why it may not work

The downside to Separation by Type is that decomposing this kind of server from a monolith to microservices architecture can be tricky since the code is so dependant on each other and there may be a number of cross package dependencies. This is where the next layout philosophy shines.

Separation by Concern

This philosophy is more common to desktop applications, however, I have come across this several times in server code, as well. When using Separation by Concern, code is separated by features or groups of features with all types of files being bundled together into a single package under a single concern. Here is an example:

An example of code separated by features

When you should use it

I find that this method usually suits projects that have slower iteration cycles and are being worked on by large groups with less connected developer pods. In these scenarios, each concern can be forked over to a different team that can internally set and implement its own guidelines for how they would prefer their code to be organized within the files.

Why you should use it

This separation has two major benefits. First, commits made to the code repositories will have fewer merge conflicts between packages since each team is focused on their own concern. Bugs that emerge within code will usually be isolated within the package itself and any cross-functional dependency that changes can be tracked independently.

Second, breaking this code down into microservices will be significantly easier since there are fewer cross-cutting dependencies. Think of this as setting up independent services, repositories, and data access layers that are more co-located and co-compiled than being tightly bound to the same monolith. For our example above, we can separate our server into two major packages, ‘Car’ and ‘User’, which can have their own style guides and architecture. Any cross dependencies and require functionalities can be exported either internally through interfaces or systems such as Feign or OpenFeign. 

Why it may not work

On the other hand, features that combine multiple entities will need synchronization between developer pods responsible for those concerns, which may ultimately slow down the iteration process and velocity of code delivery. Interfaces and cross-dependencies will need to be discussed and agreed to beforehand and changing them will take more time. 

Changes in shared dependencies will also need to go through an approval process involving all relevant teams because it may lead to their code-breaking in unforeseen ways. It’s not surprising then, that managing such teams is much more difficult and often requires dedicated team members to keep individual developer pods in sync with each other.

Conclusion

It’s imperative to get it right because it can be a costly mistake to fix later on down the road barring a full reimplementation which is quite rare without good reason. Ultimately, the ideal strategy is often a mix or compromise of the two approaches given above. 

In our experience, the best way to make the choice is to extract as much information about the project, it’s requirements and how the business side feels it will evolve. One way we’ve tried to do this at aequilibrium is to add extra time early in the software design cycle to run through a thought experiment with the engineering team to examine what the application and its development will look a month, six months, a year, and 3 years down the line. If it’s difficult to coordinate and manage the team in the meeting itself, it’s highly likely you may want to break the team down into smaller pods and divide and conquer using Separation by Concern. Hopefully, this discussion helps you make the best choice for your next software project. If you need help deciding which method to use, and building it, get in touch with our team of technical experts at aequilibrium.com.