3 things that will make or break your project

This article is about 3 things that can make or break any software project.

Now, I know what you might be thinking right now — what is this, Cosmopolitan? Also, why three, and not five or twenty?

I know, the title sounds cheesy, but I do believe that these 3 things have a detrimental effect on any software project. And when it comes to purely technical reasons why a project succeeds or fails (that is, not taking into account possible organizational issues), these things form about 90% of those reasons. Consequently, targeting these 3 things will get you 90% of the way to success (again, organizational issues aside).

#1: Following YAGNI and KISS

The first one is following the YAGNI and KISS principles.

YAGNI stands for "You aren’t gonna need it" and advocates against investing time in functionality that’s not needed right now.

Think about all the code software developers write on a daily basis, and ask yourself — how much of it is truly aligned with the customer’s needs? And by truly I mean such that the project won’t meet its functional and non-functional requirements without that code? I bet it’s less than 100%. In some cases, significantly less.

I remember myself in the beginning of my programming career. For any task, however simple it was, I tried to foresee future needs and lay out as many "extension points" as possible, just in case we need them. As a result, the application grew into a framework where the current requirements were a special case of a much larger functionality set the application was capable of. My goal was to build an application that would stand the test of time and wouldn’t require a lot of modifications even in the face of possible requirement changes.

Of course, this line of thinking is completely backwards. Extension points never turn out in right places; they impede, not help future development. A truly extensible and easy-to-change application is one where you delay as many architectural (i.e. set-in-stone) decisions as possible. You can’t know future needs. Your best bet is not to make such predictions at all.

Always build as specific solution as possible for the requirements you have at hand. So specific that it will be easy to understand and change if needed. Always ask yourself questions like:

  • Do I really need an event-sourced application where a simple CRUD-based one would do just fine?

  • Do I really need the ability to process any request asynchronously with the pause-resume functionality?

We are curious creatures and often rationalize our desire to try out new things with the "this will pay for itself in the long run" attitude. It almost never does. Don’t fall for this trap.

So, again, don’t invest time in functionality that’s not needed at this particular moment. You shouldn’t develop that functionality, nor should you modify your existing code to account for the appearance of such functionality in the future. The two major reasons are:

  • Opportunity cost — If you spend time on a feature that business people don’t need at the moment, you steer that time away from features they do need right now. Moreover, when the business people finally come to require the developed functionality, their view on it will most likely have evolved, and you will still need to adjust the already-written code. Such activity is wasteful. It’s more beneficial to implement the functionality from scratch when the actual need for it emerges.

  • The less code in the project, the better. Introducing code just in case, without an immediate need, unnecessarily increases your code base’s cost of ownership. It’s better to postpone introducing new functionality until as late a stage of your project as possible. The less code the solution requires and the simpler that code is, the better.

The KISS principle stands for "Keep it short and simple". It’s similar to YAGNI, and people often conflate the two. But, although the two principles are best applied together (that’s why I’m putting them both as #1), technically, they are not the same thing.

YAGNI is about cutting off unnecessary functionality; KISS is about making the remaining functionality as simple as possible:

YAGNI and KISS working together
YAGNI and KISS working together

It’s hard to overestimate how important simplicity is. Simple code allows you to easily understand it, which is the most important property for most applications.

It may sound controversial, but think about it this way. Which one is more important, the ease of understanding the system or that system’s correctness?

In the short term, correctness is surely more important. But as long as you continue development, readability starts to have an increasingly large effect. The situation with a bugless but unreadable code is unstable. Most likely, you will introduce bugs with future changes because you can’t fully understand that code base. On the other hand, with a simple and readable source code, you can quickly find and fix those bugs.

There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

 — C. A. R. Hoare.

#2: Implementing Domain-Driven Design (DDD)

There’s a lot to be said about tactical and strategic patterns in DDD. Those patterns can indeed be helpful (as long as they don’t contradict YAGNI and KISS), but if I were to boil DDD down to the absolute must-haves, I would say this. The most important parts of DDD are:

  • Focusing on the core domain,

  • Maintaining encapsulation.

You can choose to use Entities, Value Objects, Aggregates, and other typical DDD patterns at your discretion. Not using them is perfectly fine too. The above two practices, however, are the essence of DDD and must be followed at all times in any project.

So, what do they mean, exactly?

Focusing on the core domain

Focusing on the core domain means:

  • Structuring your code base such that it has a well-known, explicitly defined place for the domain model. The domain model is the collection of domain knowledge about the problem your project is meant to solve. It’s what differentiates your application from others and provides a competitive advantage for the organization.

    Assigning the domain model an explicit boundary helps you better visualize and reason about that part of your code. The boundary itself can take the form of a separate assembly or a namespace. The particulars aren’t that important as long as all of the domain logic is put under a single, distinct umbrella.

  • Putting effort into domain modeling. Oftentimes, when working on a new project requirement, programmers start by designing database structure, then proceed to sketching the UI and interactions with external systems, and then put all that together. The domain logic (aka business logic) is only glanced over and remains scattered across the code base — somewhere between the UI, the database, and countless "services".

    This is the opposite of how you should approach project development. Domain logic is the most important part of your application. Not only does it have to have its own distinct place in the code base, you should also put it first, before all other components.

It’s hard to overestimate the importance of the explicit boundary around the domain model. It’s not only about having a separate directory for domain classes. It’s also about isolating those domain classes from out-of-process dependencies and all other application concerns, so that it remains pure and focused.

The best way to maintain proper domain model boundaries is to adhere to a hexagonal architecture. With it, you represent your application using two layers: domain model and application services.

2020 02 03 hexagon
A typical application consists of a domain layer and an application services layer. The domain layer contains the application’s business logic; application services tie that logic to business use cases.

The application services layer sits on top of the domain layer and orchestrates communication between that layer and the external world. For example, if your application is a RESTful API, all requests to this API hit the application services layer first. This layer then coordinates the work between domain classes and out-of-process dependencies.

The combination of the application services layer and the domain layer forms a hexagon, which itself represents your application. It can interact with other applications, which are represented with their own hexagons. These other applications could be an SMTP service, a third-party system, a message bus, and so on. A set of interacting hexagons makes up a hexagonal architecture:

2020 02 03 hexagonal arch
A hexagonal architecture is a set of interacting applications — hexagons.

The term hexagonal architecture was introduced by Alistair Cockburn. Its purpose is to emphasize three important guidelines:

  • The separation of concerns between the domain and application services layers — Since business logic is the most important part of the application, the domain layer should be accountable only for that business logic and exempted from all other responsibilities. Those responsibilities, such as communicating with external applications and retrieving data from the database, must be attributed to application services. Conversely, the application services shouldn’t contain any business logic. Their responsibility is to adapt the domain layer by translating the incoming requests into operations on domain classes and then persisting the results or returning them back to the caller. You can view the domain layer as a collection of the application’s domain knowledge (how-to’s) and the application services layer as a set of business use cases (what-to’s).

  • Communications inside your application — Hexagonal architecture prescribes a one-way flow of dependencies: from the application services layer to the domain layer. Classes inside the domain layer should only depend on each other; they should not depend on classes from the application services layer. This guideline flows from the previous one. The separation of concerns between the application services layer and the domain layer means that the former knows about the latter, but the opposite is not true. The domain layer should be fully isolated from the external world.

  • Communications between applications — External applications connect to your application through a common interface maintained by the application services layer. No one has a direct access to the domain layer. Each side in a hexagon represents a connection into or out of the application.

Again, a hexagonal architecture is the best way to maintain the focus on the core domain. It’s not a coincidence that the domain model resides in the center of the hexagon.

Maintaining encapsulation

Encapsulation is another important piece of the puzzle. It’s not enough to define a model layer with explicit boundaries. You also need to keep all operations within that domain model internally consistent.

This is where encapsulation comes into play. Encapsulation is the act of protecting your code against inconsistencies, also known as invariant violations. An invariant is a condition that should be held true at all times.

Encapsulation is crucial for code base maintainability in the long run. The reason why is complexity. Code complexity is one of the biggest challenges you’ll face in software development. The more complex the code base becomes, the harder it is to work with, which, in turn, results in slowing down development speed and increasing the number of bugs.

Without encapsulation, you have no practical way to cope with ever-increasing code complexity. When the code’s API doesn’t guide you through what is and what isn’t allowed to be done with that code, you have to keep a lot of information in mind to make sure you don’t introduce inconsistencies with new code changes. This brings an additional mental burden to the process of programming. Remove as much of that burden from yourself as possible. You cannot trust yourself to do the right thing all the time — eliminate the very possibility of doing the wrong thing. The best way to do so is to maintain proper encapsulation so that your code base doesn’t even provide an option for you to do anything incorrectly.

Encapsulation can be achieved by:

  • Reducing the API surface area that allows for data modification,

  • Putting all such APIs under scrutiny.

As a programmer, you should do both. You should eliminate as many data-mutating operations as possible; and you should also carefully safeguard the remaining such operations for internal consistency.

Functional programming takes these two guidelines to an extreme and eliminates all mutable operations. With immutable classes, you don’t need to worry about internal state corruption because it’s impossible to corrupt something that cannot be changed in the first place. As a consequence, there’s no need for encapsulation in functional programming. You only need to validate the class’s state once, when you create an instance of it. After that, you can freely pass this instance around. When all your data is immutable, the whole set of issues related to the lack of encapsulation simply vanishes.

There’s a great quote from Michael Feathers in that regard:

Object-oriented programming makes code understandable by encapsulating moving parts. Functional programming makes code understandable by minimizing moving parts.

If you want to learn more about Domain-Driven Design, there’s a good set of courses on Pluralsight on this topic (of which I’ve authored most — full disclosure). If you don’t have much time getting into all the details, I highly recommend this single course of mine. It’s a relatively short tutorial that uses a close-to-real-world example to show how to refactor your application toward encapsulation.

#3: Doing unit testing

Unit testing is the third thing every project must practice to achieve success in the long run.

Just having unit tests is not enough, though. Only tests of high value are worth keeping in your test suite. There’s a lot to be said about how to build a highly valuable test suite (which I did in my recent book about unit testing), and I’ll try to lay out the most important bits in the subsequent articles.

One of the most interesting points, though, is that it’s virtually impossible to write highly valuable tests without focusing on the core domain and maintaining encapsulation.

Writing better unit tests forces you to encapsulate your domain model and build a well-designed API. Encapsulation, in turn, forces you to improve your unit tests.

Summary

These 3 things get you 90% of the way to success for any project (not taking into account possible organizational issues):

  • Following YAGNI and KISS

    • YAGNI stands for "You aren’t gonna need it" and advocates against investing time in functionality that’s not needed right now

    • KISS is about keeping the remaining functionality simple

  • Implementing Domain-Driven Design (DDD). In particular:

    • Focusing on the core domain

    • Maintaining encapsulation

  • Doing unit testing

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus