most read
Software Engineering
Why We Killed Our End-to-End Test Suite Sep 24
Culture & Values
The Spark Of Our Foundation: a letter from our founders Dec 9
Software Engineering
The value of canonicity Oct 30
Careers
We bring together great minds from diverse backgrounds who enable discussion and debate and enhance problem-solving.
Learn more about our careers



Written by: Felipe Almeida
Contributors: Arthur Kamienski, Caique Lima, Sarah Malaman, Vitor Pinheiro
Introduction
Data Science (DS) and Machine Learning (ML) practitioners rely on many tools, even though this is not always immediately obvious. Whenever we import a library into a Python notebook or run commands such as grep, find and sed on UNIX-like systems, we are using and benefiting from a tool somebody else wrote.
But we can also write our own tools. Tools usually emerge when somebody detects repeated functionality across multiple products or projects. The natural programmer instinct is to extract the common behavior into a separate place to avoid duplication and make future changes easier. Thus a software library is born.
The use of tools such as libraries (but also UI-based Applications, Platforms and command-line Applications) across an entire organization is commonplace in modern technology companies. This strategy is usually termed inner-sourcing. While it offers considerable advantages, it also comes with challenges that need careful consideration.
In this post we’ll list lessons we learned over the years using internal tools at Nubank. These lessons are not only about technical points, but also subjective issues one must heed to ensure the success of these tools, treating them as internal products.
The article is aimed at teams that build and maintain internal tools to be used by other teams, especially those with DS and ML use-cases in mind.
We’ll start by summarizing the whys of building internal tools with a brief overview of the main types, then list the lessons we deem most important, in no particular order.
Check our job opportunities
Why build internal tools?
We build tools because they make us more effective and efficient. This is true at the individual and especially at the organizational level, as there are many emergent and nonlinear phenomena that only take place once you reach a certain scale.
Tools drive standardization
Tools increase the impact of local solutions
Tools encode tacit knowledge
Tools reduce risk and mistakes
Tools decrease TTM
Types of internal tools
At the end of the day, software tools are just another piece of code. But if we look closer, there are different types of tools with respect to the interface they expose to users and the capabilities they provide. Each of them plays a separate role in a tech organization.
A key distinction is that of tactical vs strategic tools: those that are usually used within at most 1-2 teams and those that can be shared by the whole organization — potentially becoming a key asset for a company.
Figure 1 below summarizes the main types: UI-based and Command-line Applications, Libraries and Platforms.
It’s easy to see the difference between UI-based and command-line applications, but this is not the case with libraries versus platforms: One key difference is that platforms usually require one to go all-in on them. You can’t normally pick and choose which parts of a platform you want to use — you either use it all or you don’t. Compare this with libraries: you can use multiple libraries at the same time, mixing and combining them in multiple ways, but once you choose a platform you stick with it.
Another difference is that a platform is usually opinionated; clear design choices are made and you need to embrace them if you choose to use it. For this reason, platforms are great to enforce standardization and organization-wise patterns that all teams should adhere to.
With the intro out of the way, let’s dive in on the actual meat of the article.
Internal tools are products and should be managed as such
Internal tools are products and must be managed as such. You must think of internal users as customers — which is sometimes hard due to the proximity and informality of day-to-day operations.
Ideally you should have dedicated product managers understanding and interacting with users — and setting up priorities based on what they need. If that’s not possible, there should at least be people with product management skills in the team.
The bare minimum you need to manage an internal product are: a clear roadmap, structured feedback from users, and a good support channel.
Examples and good documentation help reduce the need for 1:1 support. Also, make sure users know how to search Slack for answers to frequent questions.
Evangelization is key
Hardly anyone wants to learn yet another tool — especially if the advantages aren’t immediately apparent. We must actively practice evangelization, promoting the tool to prospective users.
In some instances, the adoption of a tool might be mandated through top-down approaches, such as incorporating it into Objectives and Key Results (OKRs) or other corporate goal-setting frameworks. In these scenarios, there’s probably not a lot of need for evangelizing. But that’s not our focus here: we’d rather build tools users want to use, because they see value in it — not because those are forced upon them.
From the point of view of customers, using a new tool means having to learn it and maybe change some aspect of their work.
This can be a significant barrier, because people generally resist change, especially when it involves modifying a workflow they are accustomed to and proficient in. Several factors are at play here: perceptions about job security, ego issues and perceived loss of influence. People don’t want to let go of something they spent 2 years mastering, especially if they think that confers them status within the organization.
In our experience, it’s better to show value first. If you do it right, the decision to adopt your tool will be obvious and easy. Here is how:
Logging, instrumentation and user feedback
When building internal tools, there is a real risk we’ll just assume we know what users need and how they’re using our tools.
Use data instead. Without data, product management is reduced to hunch-based decision-making, rendering it a purely political endeavor where the highest-paid person’s opinion (HiPPO) wins.
Collecting information enables the maintainer team to understand how users are using the tools: it helps gauge customer satisfaction but also detect focus areas you should concentrate efforts on — making maintenance more effective.
We see two main ways of collecting information: implicitly and explicitly, via system instrumentation and surveys, respectively. Each provides insights from a different angle and both have advantages and disadvantages, as we’ll see next.
Collect data implicitly via logging and instrumentation. Every programming language supports logging, which can then be visualized in tools such as Splunk or Grafana. The main advantage of implicit feedback is that it’s impossible to fake — but it takes work to set up.
Here are some examples of what information can be obtained via logging:
On the other hand, user surveys are good for explicitly obtaining feedback -– as long as users feel comfortable about it. The main risk is that internal customers may avoid giving honest and sometimes negative feedback due to fear of office politics and retaliation. User surveys should be anonymous to avoid that.
When choosing surveys à la Google Forms, be mindful of your customers’ time: Pack as much as possible into a short form — people have better things to do than answering a 20-page survey on your product.
Prefer questions with numerically-ranked answers (e.g. Likert scale) to enable comparisons over time and be as specific as possible: “On a scale of 1 to 5, how much would you say tool X has increased your productivity when doing task Y?”.
Finally, make sure to include 1-2 open-ended questions to allow general opinions and advice from users.
Examples are the best type of documentation
Having documentation is a common requirement for internal tools — especially when you need to cater to nontechnical users.
However, good documentation costs a lot to write and especially to maintain. Adding features to tools will always have priority over updating the docs. So the tools evolve and documentation lags behind.
The usual path for tool documentation is to grow stale over time and die a slow death, eventually getting to the point where nobody trusts it anymore. Sometimes it even becomes outright misleading: instructions that don’t make sense anymore, reference to features that have been changed, etc.
Also, nobody wants to read documentation. Users want to get things done as quickly as possible. The fastest way is usually by looking at examples and trying to figure out how to adapt those to their own use case. Make it easy for them by having a set of canonical examples as part of the official documentation.
In addition to the benefits above, a good suite of examples also help reduce support effort. It saves the support teams from having to address questions that could be answered by pointing at an example.
Examples can even double as integration tests for tools — thus making sure the examples are always validated against new builds, as part of your CI/CD flow. Your documentation is now testable — which is the best of both worlds, preventing it from becoming stale.
Finally, examples help prevent “bad practices” from propagating among the user base. You’ll be surprised by how many users copy and paste other people’s code in an attempt to make things work.
Examples are the best type of documentation, especially for internal tools to be used by technical folks such as data practitioners. They are easier to create, validate and maintain than written text; they are closer to the source of truth (the source code) and quicker to consume. Examples are second-only to the code itself.
Maintain Consistency
When someone uses a tool they need to build a mental model of how it works. A simple example is the pedal setup in manual cars.
Car brands vary widely with respect to colors, sizes, and even the side the driving wheel is located: but they are consistent with respect to the pedal layout: once you’ve built a mental model of how to use them, you can rely on it no matter what car you drive. This can be seen in Figure 2 below.
Now imagine if every different car brand had a different pedal setup. When driving a new car, you would never know what to expect and you’d have to build a whole new mental model and re-learn how to drive it.
Internal tools such as libraries and platforms are also like that. The more consistent a tool is — using the same patterns, names, structure and conventions everywhere — the faster users can build a mental model, and the more intuitive the tool will feel.
This helps prevent errors, reduces the need for support and, most importantly, decreases the cognitive burden of your tool. Using a consistent tool means that once you’ve learned how to accomplish one specific task, you’ve learned them all – as is the case with cars and pedal setups.
We all know tools that feel easy and intuitive and those where you can never quite remember how to use (and need to constantly google for examples). Pandas and Matplotlib are examples that come to mind here: although very good libraries there’s some inconsistency in names, argument order and function semantics.
A good rule of thumb to keep in mind is the principle of least surprise, whereby you, the tool maintainer, always choose to structure things in a way that feels more natural, or less surprising to the user.
The biggest advantage of consistency is to decrease the mental burden of users, but we have found it even helps in discoverability: users are better able to explore your tools if things are consistently named and structured.
Here are some practical tips on how to achieve consistency in internal tools:
FooBar(in CamelCase), and the corresponding database table should be calledfoo_bar(snake case).Beware of Dependencies
As people start using tools you build, they will inevitably start to depend on those to get their job done. If the tools change in unexpected ways, this will impact users and prevent them from working.
This is actually a good problem to have; it means that people are extracting enough value from your tools to make it part of their workflows. The only tools without dependency concerns are those that nobody uses.
But dependencies are a problem because they limit the ability to change and enhance the tools: updates may break workflows that currently depend on the tools.
Like any software, internal tools need to change and evolve as more features are added. However, the more tightly users have coupled their workflows with your tools the harder it’ll be to add features and make the necessary changes to combat entropy and software rot.
You must limit coupling between users and tools, so that when the time comes to add new features and refactor, you can do it safely.
More specifically:
Hyrum’s law is an extreme interpretation of the dependency problem; it states that clients will depend, not only on the public API your tool exposes, but on any observable behavior that can be inferred or seen from outside.
The law was originally about dependencies between software modules, but the analogy is still valid; it’s just impossible to guarantee that there’ll never be breaking changes as you update your tools. But still, there’s a lot we, the tool maintainers, can do to reduce the risks by keeping APIs stable while making sure the tool is updated as needed.
80/20 Thinking
Good tools do not try to embrace the world. They strike a balance between how much freedom and how much constraint they expose to users: too few constraints makes tools too unstructured and hard to use; too many, and advanced use-cases will be left out.
You should focus on making sure that the 80% of common tasks just work in a natural, intuitive, and efficient way. But you must also leave space for the advanced use-cases — the remaining 20%. This means supporting some level of customization to cater to advanced users who will have complex and sometimes unorthodox use-cases you had never previously considered.
Users will always find a way to get their job done. If they cannot adapt your tool to their use-case, they will not use it. Full stop. It is your job as the tool maintainer to write a tool that just works for the simple use-cases but can still be customized for advanced ones.
Inner-source tools are open-source within the organization; so the first way to encourage customization is to follow good SE practices and write clean code, so that users can, themselves, understand it and eventually contribute Pull Requests (PRs) to the codebase.
Three strategies have been particularly useful for us:
Object-orientation enables easy overriding of default functionality
Object-oriented programming isn’t suitable for every use-case, but it works well when you want to expose components that can be extended by users if need be. This is even easier in dynamic languages such as Python (the lingua franca of the DS/ML world) where you can extend core classes and override default behavior at runtime by simply adding methods.
This has proved useful on multiple occasions; many advanced users can code so they hack their way into achieving whatever they want by creating slightly modified subclasses in their own code.
Sensible defaults
Most tools have configuration options; It can be a config file, “settings” screens on an app, or something like that. Configuration options enable user customization, so you definitely need them. But you don’t want users to have to set configuration options for every single task.
Configuration options should have sensible defaults that cater to the majority of cases — while enabling customization when appropriate. Deciding which default values to use requires you to understand your average user and how they use your tool. Find these out by interviewing them and studying their needs.
Ease of use over code elegance, every time
When you need to choose between ease of use and internal code elegance, favor ease of use — even at the cost of some technical debt.
Losing users is harder to recover from than a temporary loss of code quality, which should be easy to fix if you isolate user-facing glue code from the core business rules, as we suggested in Beware of Dependencies section of this blog post.
Conclusion
As we saw in the post there are many ways to avoid — or at least mitigate — problems when building and maintaining internal tools, especially for technical users such as Data Scientists and Machine learning Engineers.
If we had to summarize the lessons as a short TL;DR, we’d say:
Check our job opportunities