Cross-Training for Software Engineers

Disclaimer: No Exercise Required

We don’t often make the comparison of software engineering to running, but in my time doing both, I’ve learned that cross-training is just as important for developers as it is for runners. Competitive runners cross-train by targeting exercises that provide different benefits than running: lifting weights increases strength, plyometrics improve agility, and swimming reduces the risk of overuse injury. Swapping out a few runs for cross-training doesn’t detract from their running — in fact, the variety makes them better athletes.

Similarly, the variety of cross-training will actually make you a better software engineer. Working on multiple similar software projects concurrently rather than just one will help you to consider different (and possibly better) ways of implementing your changes. By understanding the unique features and structure of each project, you’ll gain a deeper understanding of the problem space, and you’ll be able to use that understanding to compose the best possible solution for each individual project.

To demonstrate why this is true, let me tell you a story — a story involving two open-source web frameworks (Flask and Django), one standard distributed tracing API (OpenTracing), and one developer (…me).

Developing a Flask-OpenTracing Extension

The beauty of Flask is that it’s an incredibly simple web framework to learn. In fact, it’s not even considered a full framework, but rather a microframework, since it leaves most design choices (databases, authentication, structure, etc.) up to the user. Most of its functionality actually comes from third-party extensions instead of its core, which made it a great first framework to write an extension for.

Python, the language that Flask uses, has convenient decorators that can wrap methods to extend their functionality. For example, if you want to generate italicized HTML, you could create a decorator @italicize that would return any text wrapped with italics tags as follows:

Using this decorator on hello_world() results in:

> print hello_world()
<i>Hello world!</i>

This is perfect for integrating OpenTracing, since its goal to create spans that represent the lifetime of requests to the server, which in Flask are handled by view functions. I created a function decorator @tracer.trace that would, in short, create a span when a view function was called and end the span after the function returned.

The end result was an extension that allowed users to integrate OpenTracing in their Flask app with one import, one tracer initialization, and a decorator on any view function they want traced. Running the following app and navigating to `/traced-path`, for example, produces a trace.

Traces produced for a traced client request to `/untraced-path` (top) and `/traced-path` (bottom).

Moving on to Django

Django, on the surface, is incredibly different from Flask. It has built-in features and a lot more complexity — while a Flask application only requires one file with five lines of code to get started, basic Django apps are separated into several files (settings, urls, views, etc.) and have a built-in MVC structure. Of course, while this does require more overhead to get a minimal application, it also allows for more straightforward scaling of apps than Flask.

However, these differences are largely user-facing, and I found that from a contributor’s perspective, Django and Flask are actually remarkably similar. My first iteration of Django-OpenTracing was, in fact, nearly identical to the Flask extension. Django uses view functions to handle requests and is written in Python, which meant that the @tracer.trace decorators that I had previously developed for Flask, after some minor tweaks, worked in Django as well.

After initializing a tracer in

OPENTRACING_TRACER = DjangoTracer(some_opentracing_tracer)

tracing of requests could be added to as follows:

So we’re done, right?

Well, we could be. But the awesome thing about Django and all of its built-in features is that it has a concrete concept of middleware, which controls the request-response processing of an application. Rather than just using tracing decorators, I additionally created the following OpenTracing middleware class that, when registered in, traces all requests to the application.

If we already had a working OpenTracing integration for Django using decorators, why add another one using middleware?

The answer is that more options are (almost) never a bad thing. If you want fine-tuned configurability or to decrease overhead by only tracing “problem” view functions, then the tracing decorators are great. However, if you have hundreds of view functions and don’t want to manually add @tracer.trace to every single one, then you can initialize OpenTracingMiddleware in one line and trace everything automatically. Providing developers with two styles of OpenTracing integrations allows them to choose the one that best fits their needs.

So now we’re done, right?

Nope — we’re just getting to the good part.

This experience developing the Django-OpenTracing integration got me thinking more about my integration in Flask. If I could develop two styles of tracing in Django, could I create this additional functionality in Flask?

As it turns out, yes. While Flask doesn’t have middleware in the same sense that Django does, it does have a way to add custom steps to request-response processing. There are two provided decorators, @app.before_request and @app.after_request that you can use to define methods to be executed — you guessed it — before and after each request.

The new release of Flask-OpenTracing now automatically traces every request to your app through one initialization:

tracer = FlaskTracer(tracer=some_ot_tracer, trace_all_requests=True)

Ok, we’re done.

So what was the point of this story?

Well, I developed OpenTracing support for both Flask and Django. But I also learned a lesson — that you can learn a lot about a framework by looking at other frameworks and comparing the differences between them. Developing packages for multiple frameworks not only made it easier to do subsequent integrations but also directly impacted and improved each other’s functionality.

There are a lot of benefits to cross-training as a software engineer. By working on multiple related yet distinct projects at the same time, you’ll gain experience, efficiency, and a deeper level of understanding of what you’re working on. Perhaps most importantly, though, you’ll achieve a wider impact. Take open source projects like Flask and Django for example: if you use cross-training to make more, higher quality contributions, it will improve the development experience for a whole community of users. And at its core, maximizing positive impact is what becoming a better software engineer is all about.