Time-Driven Development: The Way to Software Development Efficiency

by Kate Trajchevska

13 min read

Time-Driven Development is a software development concept where the main focus is development time. Here's everything you should know about this approach and how you can make it work for your team.

Being a software engineer nowadays can be overwhelming: you need to keep up with the latest trends in software development, always follow “the best” software design practices, and make sure you earn the respect of your Twitter fellows. 

A couple of years ago, Johannes Broadwall, fed up with the attitude everyone (including himself) had towards Clean Code, he went so far as to boycott clean code for the sake of compassionate code.

As he said, he was fed up with leaving the office later than his colleagues every day, to spend time “polishing some piece of ultimately meaningless code”. 

On a similar note, a Stackoverflow user, who was desperately trying to make use of the SOLID design principles, fragmented their code so much that they ended up asking for a way to refactor away from SRP.

When the best practice makes your life as a developer miserable instead of better, it’s not the best practice anymore, is it?

We desperately need a new way of defining the best practices in software development depending on the context of the project. That’s what Time-Driven Development is about: designing our code in a way that saves most time in the long run.

What is Time-Driven Development?

Time-Driven Development is a slightly different software development concept where the main focus is development time. And this doesn’t mean that you should ship features fast without thinking about code quality.

Quite the opposite: before deciding on the design of your code you should think about how it should look like in the long run.

If it’s a feature that should be there years from now and needs to be extendable and maintainable, then go ahead and plan your code architecture in a way where you’ll spend more time now in order to save more going forward.

However, if you’re building an MVP or a single feature that’s not interconnected and you’re not sure how it will get developed going forward - spending time on the perfect architecture is a time that you could spend on creating more value for the user.

Let’s look at some examples.

How to Save Time by Following Good Design Practices?

Before we became a fully remote network of engineers and designers from all over the world, Adeva was a small agency with a team of 10.

When we first started working remotely and expanding to new countries, it was getting harder to manage things in the way we were used to.

So, we started working on building an infrastructure that will enable us to manage our global team and easily give companies access to the best talent everywhere.

We didn’t want to spend too much time or money initially on building this platform, so we took things lean. We were using Zoho CRM at the time, so we said ok, this will be the base for our platform.

Zoho’s infrastructure was our storage for all candidates' details and we created a nice interface on our side where people could log in and manage their profiles.

We heavily relied on Zoho’s API and looking to do things fast, we created a single class where we dumped all methods we needed to manipulate with Zoho’s data and pass them to our app. It looked something like this:

class ZohoCandidate {
    public function getCandidates() {...}
    public function getCandidate(int $id) {...}
    ...
    public function getExperiences(int $candidateId) {...}
    ...
    public function getEducations(int $candidateId) {...}
    ...
}

Then, in the CandidateController we simply get all details from the CRM class and we’re good to go.

$zohoCandidate = new ZohoCandidate(env('ZOHO_AUTH_TOKEN'));
$candidate = $zohoCandidate->getCandidate($id);
$candidate = $zohoCandidate->getCandidate($id);
$educations = $zohoCandidate->getEducations($id);
$skills = explode(', ', $candidate['Main Skill']);
return view('profile', compact('candidate', 'experiences', 'educations', 'skills'));

This all works fine, but it relies very heavily on the CRM, which can be a problem if you plan to move to your own database storage at one point and stop using the external CRM, like we did. If we follow the SOLID design principles, we’ll notice a few red flags:

  1. The controller knows too much about the implementation of the CRM storage. It instantiates the class directly and uses its methods, which makes it hard to change the implementation in the future. If we need to change the storage to MySQL for instance, we’ll need to come back to this method and change the whole implementation. This makes the controller open for modification and very error-prone.
  2. Our app fully relies on the CRM’s storage and variable names, which makes it hard to change the implementation with local storage with different naming conventions. When we want to change the storage, we’ll need to go ahead and change the variable names throughout the code.

Taking a diff view from Github, this is how changing the storage looked like:

Git diff: not following SOLID design principles

And this would’ve been ok if it was just one file, but the same issue translates to all other methods for managing the candidates; all controllers handling related data, views, and unit tests. Changing the storage from the external CRM to a local database turned out to be quite a quest. Here are the things we had to do:

  1. Create classes for managing the local database storage (in our case, we’re using Laravel so this meant creating Eloquent models to use with MySQL).
  2. Change all dependencies from CRM class to Eloquent in all controllers throughout the app. 
  3. Use Eloquent instead of CRM to pull the data for presenting it to the users.
  4. Switch all variables to the new naming convention defined by our local connection. 
  5. Change the way we access data in the views (use eloquent relationships and new naming conventions).
  6. Adjust the unit tests to work accordingly.

Considering all of this, the time we estimated we’ll need to switch the storage was 30 days. The question is: can we do something to save time? Turns out, we can. If we improve the code design to allow for extending and changing the implementation easily.

How to Design Your Code to Help You Save Time in the Long Run?

Designing your code in a way that will save you significant development time in the long run requires you to invest some extra time in the beginning to create a solid architecture that allows for easy extension and maintenance of your code. Here are some steps that we took which can help you achieve that.

Don’t Depend on Concrete Implementations

One of the ground rules of the SOLID design principles is to never depend on concrete implementations and depend on abstractions instead. This gives you the flexibility to change the implementation in the future without having to change the whole code.

In our example below, what we need to do to achieve this is to create an abstract class or an interface that will outline all methods we need to use from the CRM and have our CRM class adhere to that. Our controller would then depend on the abstract class instead of the concrete CRM implementation.

Here is how that looks like:

public function edit($id, CandidateRepositoryInterface $candidateRepository) {
    $candidate = $candidateRepository->getCandidateById($id);
    $skills = explode(', ', $candidate->overview->main_skill);
    return view('profile', compact('candidate', 'skills'));
}

Be Mindful About What Your Program Expects

To be able to replace one implementation in our codebase with another, it’s important to be aware of what our program expects.

In our case, we relied on the CRM’s methods and naming conventions so simply replacing the implementation with local storage would’ve broken our program immediately.

To avoid this, it’s a good idea to think about what your methods or variables should look like before starting the implementation.

That way you can easily change the implementation afterward without having to change all instances throughout the code. Let’s see what this means in our example.

First, we had to think about the way we’ll want to access the data when we change the storage to a local database. Since we were going to rely on Laravel’s models, we decided to create an adapter class to mock their behavior.

Now, instead of having one big CRM class, we had small classes for all of the data records the CRM returned, simulating the functionality of the Laravel models.

class Candidate {
    ...
    public function overview() {...}
    public function education() {...}
    public function experience() {...}
}

Then, we need to account for the variable names. So, the adapter class should adapt the variables the CRM returns into the format we need. With this, we can use the API in the same format we’ll need when we change to local storage.

private function parse() {
    return [
        'CANDIDATEID' => 'id',
        'First Name' => 'first_name',
        'Last Name' => 'last_name',
        'Salutation' => 'salutation',
        'Email' => 'email',
    ];
}

Add New Code Instead of Changing the Existing One

Having adjusted our code like this, we can now change the storage from the CRM to our local database without having to change all of the underlying code.

Instead what we need to do is add new classes that will cover the MySQL implementation and change the dependency from ZohoCrm to MySQL. Here is what we did to achieve this:

  1. Create a new repository LocalCandidateRepository that implements the interface we outlined for the Candidate methods.
  2. Create new classes for each of the local tables we’ll use in the database once we move to our local storage.
  3. Bind the  LocalCandidateRepository with the CandidateRepositoryInterface, so every time we try to make an instance of CandidateRepositoryInterface it now uses the local repository instead of the CRM. In Laravel, this is done in the AppServiceProvider, with the following line of code $this->app->bind('App\Repositories\Interfaces\CandidateRepositoryInterface', 'App\Repositories\LocalCandidateRepository')

Once we do this, everything works out of the box. We don’t need to go to all controllers, views, or unit tests and change everything to work with our new implementation. This makes our application much more resilient and far less error-prone. And what’s best, with this new implementation we reduced the time needed to change the storage from 30 to 5 days.

When to Follow the Yagni Principle?

Inspired by how much time the approach above helped us save, we couldn’t wait to apply it again in a different problem. So, when we needed to add the ability for our managers to review additional details for our applicants, like code repositories and similar achievements, we grasped the opportunity.

The task was fairly simple: get some details from the code repositories and contributions for our candidates. We figured we’ll start with Github and in the future extend it with Gitlab and Bitbucket.

Then, we set up our infrastructure similarly to the one above - create repositories and adapters to make our code closed for modification and easy to extend and change.

We had an AbstractVersionControl interface that outlined all methods each of our respective classes should have just like above. Our GitHub class implemented that interface and so would all new classes that we planned to add in the future.

Then, we added another layer of complexity and created a VersionControlFactory that initialized the proper class depending on the user’s selection. For starters, it only had Github as an option:

class VersionControlFactory {
    public static function initialize($type) {
        switch ($type) {
            case 'GITHUB':
                return new GitHub();
                break;
            default:
                throw new Exception("Version control not found.", 1);
                break;
        }
    }
}

Our controllers were clean and didn’t care how things were implemented in the background. Our code was easy to extend and maintain. SOLID proponents would be proud of us!

What happened next is months passed by and we realized that we never had the need to extend this module further. We spent a lot of time at the beginning setting up this nice architecture just to realize that we didn’t actually need it.

On top of that, we ended up with a complex code that was hard to read and understand because it added a lot of complexity to something that could’ve been done in two lines of code. What was supposed to save us time in the long run ended up wasting our time now without any future benefits.

In a case like this, we’re better off relying on the YAGNI (You Aren’t Going to Need It) principle than the SOLID design principles. Just take a bit time to think whether you’ll actually need it before you start investing so much time in outlining “the best” code design.

How Can Time-Driven Development Make Your Development Processes More Efficient?

The goal of Time-Driven Development is not to ship fast without thinking about the consequences. On the contrary, it’s about deciding on the best practices depending on the context of your project, with the goal to save the most development time in the long run.

Software design principles come with trade-offs. Sometimes you need to spend a bit more time at the beginning to make your project easy to maintain and extend. At other times, making the same investment upfront can be a waste of time and result in overly complicated code. 

How to Make Time-Driven Development Work for Your Team?

Here are some steps you can take to make sure you make the best use of the concept of Time-Driven Development in your team.

  1. Think about the big picture and the business goals before deciding on the right architecture. Are you 100% certain that the feature will need to change or extend in the future?
  2. Think about the trade-offs of the approach you want to use. Would you save more time by investing a bit more in the code design at the beginning, or by starting simple and refactoring later if needed?
  3. Keep the context in mind when designing software. What’s considered a best practice by others doesn’t mean it’s best for the state of your project.

FAQs

Q: What is Time-Driven Development?
Time-Driven Development is a software development process where the main focus is development time. It requires the team to think about the context of the project and design the architecture of the software in a way that would save the most time in the long run.
Q: Why should I use Time-Driven approach?
Software development principles come with trade-offs. So, instead of aiming to achieve them as our goals, we should use them as tools that make our development process more efficient. With the time-driven approach, the focus is not on what you use, but how you use it. Is SOLID helping you achieve your goals faster and more efficiently? Great, double down on it. If not, maybe follow YAGNI this time and refactor later.
Q: How to start practicing Time-Driven Development?
  1. Think about the business goals of the project and how it would look like a year from now.
  2. Think about the trade-offs of the architecture you propose. Will you need to spend more time at the beginning to save more later on? Is it worth it?
  3. Never aim to achieve a perfect code, but great developers' experience and efficient processes.
Kate Trajchevska
Kate Trajchevska
CEO of Adeva

Katerina is the co-founder and CEO of Adeva. She is an advocate for flexibility in the workplace and a champion for equality, inclusion, and giving back to the community. Katerina has a proven track record of building and leading cross-cultural teams and eliminating chaos in product and engineering teams. She is a conference speaker, where she shares her vast experience and insights in managing engineering teams and navigating the future of work.

Expertise
  • Software
  • Product Strategy
  • Leadership
  • Planning

Ready to start?

Get in touch or schedule a call.