Being a startup CTO in Stuttgart turning the boat towards dependency injection and inversion of control

Jonas Schweizer
Dec 19, 2019 · 8 min read
Know your tools, start using them, learn on the way and see things evolving

If you don’t know anything about Dependency Injection (DI), you might start here or with a more classical approach read about SOLID and clean clode :)

As one of the fastest growing Manufacturing-as-a-Service (Maas) start-ups in Europe, we are facing a very common and known challenge — balance feature speed and long-term code healthiness. At 2,5 years of age, we took action in a start-up way.

TLDR — what this article covers

  • Things we considered before making the move with DI (CTO perspective)
  • Patterns we used and our component architecture (Architect perspective)
  • Code samples in TypeScript, InversifyJS + Sequelize (Coding perspective)
  • Milestones mitigating Big Bang refactorings (Start-up perspective)
  • A list of learnings along the way

What this article does not contain

  • Pros and cons of functional vs. OO programming. We will use classes and interfaces to achieve DI and IoC, functions are fine as well
  • An in depth comparison of DI frameworks, although we will mention awilix and InversifyJS

The value of properly managed dependencies and contracts

Me personally as CTO, I am a huge fan of only investing in things that have direct impact on our customers, product or our team. I am 100% confident that this measure of introducing DI will dramatically impact our development flow. Short-term it is quite some effort with little external value. But Laserhub is not a kid anymore and sometimes you have to look ahead.

On the other hand side the biggest impact and value is a passionate and structured engineering team. Investing heavily in it is always right. In our case, the team was a big driving force and we “only” had to find the right way. While we were investigating some alternatives, any discussions we had, resulted in a better understanding of the current strength and weaknesses of our platform. Further, people learned from each other, pointing out opinions, perspectives and experience. That already adds value before even writing a single line of code.

All that soft things laid out. Dependency Injection and Inversion of control is a huge step towards better testability, clearer responsibilities of code and way better maintainability. I will try to elaborate on this while writing.

Decoupled code, but wait the ORM …

Overall, the most important thing that we wanted to achieve was the creation of decoupled components with clear boundaries in the form of a properly typed service layer.

We faced this mainly on the ORM part of our system. While we always tried to look out for proper component structure in our code base, we were a bit lazy on the ORM part. We use Sequelize for most of our persistence logic.

While Sequelize gives you create opportunities to associate tables in code and do intuitive and convenient DB access, it also drags you into creating a monolith on DB layer. Convenient short-term — a monster long-term. So this simple Association Feature of Sequelize is sth. you might consider a bit earlier than we did.

So it was time for a change. We knew we wanted to change, but wait: HOW do we do it with all these JS libraries laying in front of us.

How we kick-started this new architecture

In our typical

Think It – Build It – Ship It – Improve It

kind of working, we started with some pragmatic research. Part of our strategy in engineering is being fast followers and best of breed. We are looking for tools, framework, platforms and software which fit our main needs, are ready for production, well maintained and easy to integrate.

This is the kind of reasoning I miss from current tec articles. While there are lots of tutorial on implementations details and some articles comparing frameworks, usually there is little coverage in the actual things being considered when making/buying a new technology.

So we knew, we want to invest in DI, but we didn’t not have a proper tec design for everything else. Well, the general concept is rather clear and there exist hundreds of articles for specific tec stacks and their frameworks.

While reasearching you come across general principles and we knew that Inversion of Control (IoC) will be part of the game. Further, we had a shortlist of Bottle.js, awilix and InversifyJS as popular frameworks to use.

Bottle.js was so lean and kind of straightforward, it lacked some features.

Awilix and InversifyJS both featured different ways of injecting dependencies (as value, as class, constants, etc), using DI on req/res level to inject properties on runtime (i.e. a user‘s language) and by this showed us that these tools will cover most of our needs out of the box.
InversifyJS was backed by major cloud companies, definetely a plus.

Time spent on research: not more than 4 hours I would say.

Initial POC

Ease of use was tested by implementing it for a subset as an initial PoC. Awilix and InversifyJS were implemented by two different engineers in our team and we had a detailed discussion on both experiences.

Learnings:

  • Introducing TypeScript partially worked out well for us
  • Go all the way down to unit test level and see how much you improve testability in real life and how much effort it is
  • The discussion afterwards revealed main architectural ways to go, like: a central place for DI Container is perfectly suitable, you don‘t need a separate container for testing (for now) as you can override dependencies easily and you can easily decide step by step how far vertically you wanna go with the DI pattern

While we were hesitant to introduce TypeScript as some kind of pre-requesite for InversifyJS, the popoularity of the package made the final impact.

Awilix is even a bit easier and very smoothly integrated into an existing app, InversifyJS seemed to be better supported.

So InversifyJS it was, general principles were decided, now let’s put in action.

Avoid Big Bang refactoring — achieve early value

Going back to Ship It, I personally dont believe in Big Bangs. So we started with one clearly defined and then implemented component. Do all the learnings there and already have a better designed component #2.

We started with a single, rather broad component — in our case the AccountComponent. We completely setup everything needed to make it a shiny DI component. Previously we had several different services that were developed over time: users, producers, companies, roles and permissions. All of these logic is and most probably will be thightly coupled — or put different. The reason for a change might be similar. That’s why they represent a single component.

One key concepts in approaching this: While we could have cut down every of this micro services into very detailed component, only managing their exact single reponsibility — we went for a macro component. This component holds many services reflecting similar cases as before: roles and permissions, users, etc. The main difference: DB associations accross services within a single component are allowed. But the main purpose of clear boundaries to other components is kept alive. This makes it way easier to refactor existing code.

account services before and after (right side shows broader account component)

On the other hand, plenty of other services were using, i.e. old userService.js. Some of them were using import statements. These had to be changed. Others already used constructor based DI (well we were not completely wrong 1 year ago — yeahi!)

The good thing is: we didn’t have to update latter ones as everthing the DI container gives us is a plain JavaScript object that we can use to inject into pre-existing services.

registering components in dependency container (the proper way)
how services are loaded with legacy code and using DI even in legacy controllers

As you can see from the examples, a transition is easily made. You start using DI for a subset of components and instantiate your services in the container. You can gradually move into DI file by file if you want to.

But wait — why are we doing this again

So while we change little on the service layer, digging deeper shows the main purpose. Getting rid of all DB level associations across components requires more effort.

Before we could use Sequelize associations easily as

This breaks our component boundaries. Instead, in future we will do

In many cases, this is an easy change. In some cases this requires a bit more thinking. Still … managable. After making all those changes, the time has come. We can cut cords

Our user entity is now de-coupled from the order entity on DB level. It has still associations to entities within the account component but not across component boundaries.

At this point, our team managing the account component can now make sure to keep all service APIs stable while changing any inner workings.

Effort so far: roughly 5 days, including initial research + setup of TypeScript, InversifyJS, etc.

A close sister

We used DI and InversifyJS to make an effort to de-couple our components. You probably recognized that we injected interfaces instead of actual userService implementation. A close sister to Dependency Injection is Inversion of Control. Relying on interfaces hides implementational details of the dependend component.

Now, Inversion of Control is applicable on all layers. As stated in the beginning, our main focus was to clean up service layer and avoid cross-component DB associations.

Now with the new power of DI at hand, we went one step further. Our services grew too big over time. One simple measure was to create another layer within our components — repositories.

Repositories are a layer between business logic and persistence. Compared to Entities (lowest level in code), repositories know more than one table/collection, i.e. user and role. Repositories allow querying for data and respond with plain data.

Once again we made use of IoC by creating IRepo interfaces allowing us to make repositories easier to inject and test.

At this stage, we made another pragmatic or shortcut decision. Going all the way with DI, we would have taken into account entities. Making that properly, we would have to create an interface and maybe even proper TS type definitions for each entity. On the other hand, entities and repositories are strictly bound anyway. Changing persistence technology (i.e. SQL => NoSQL) requires efforts in both places. For us, it was not worth the effort to create a lot of boilerplate for this level.

So to put it simple: clean service layer, semi-clean component architecture

Once again: little effort, great value. Effort was basically creating new files, changing some imports and copy/pasting code.

Summary and learnings

Looking back our main learning is: a paradigm of constant change allows you to change your code when needed and at the point where you know what’s necessary.

A limited scope of your refactoring makes things fast and light weight. This task was an effort of one engineer in parallel to other tasks (not a perfect setup to be fully transparent). Still, we had little merge conflicts as the scope was clear and changes acrosss the platform were limited.

We shared our milestones across the engineering team in regular code reviews, allowing them to ask questions and provide hints. Further, this made sure everybody was aligned and will be able to contribute later.

Testing this was limited to the account scope and was done during usual release trains.

Overall it was fun to investigate this, build it fast, shipt it and now improve the rest of the backend.

For sure, not having refactored everything end-to-end poses a risk of inconsistencies in the code base, but that’s something we kind of live with anyways — bringing us back to constant change.

Next step you ask: Act on the bigger components and move the to the same level of code sanity and build our new components de-coupled in the first place. This is possible as we understand our business domains and requirements way better than two years ago.

Sounds familiar? Drop me a line.

Jonas Schweizer

Written by

Non-serial founder, digital enthusiast, software all-rounder, socializr, Hip-Hop supporter and proud father.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade