This article is a plea for self-improvement. You can do this. Be an engineer.

As always, caveats first: Engineers definitely should and do use frameworks. They're beautiful bits of engineering that get stuff done in a maintainable way. Frameworks are not the enemy of this article. Bravo, frameworks. OK, enough of that.

Sorry, what are frameworks? Frameworks are software tools that provide a scaffolding to complete software projects of a particular type. So if you want to write a single-page web app in TypeScript, you don't have to do it from scratch because there's Angular. Want to do some Machine Learning in Python? Allow me to introduce my friends Scikit-Learn and Keras. Want to write a backend in C#? (Oh my, you're very hip.) I'm sure you already know about ASP.NET. I could do this for the next 1500 words, but you get the idea.

If you know a framework, you can often get a job that has the word "engineer" in the title, and possibly "machine learning". If you know two frameworks, you might get a job that also has the words "full stack" in the title. But your skill set needs to go much deeper than frameworks if you're going to have success in your next job — the one where you get hired because you had 3–5 years of "engineering" experience on your resume. Otherwise, you're going to be sitting in a very uncomfortable chair at that 90-day review.

You may need to go on a journey. From Frameworker to Programmer to Engineer. Let's look at each of these phases of the journey. I'll describe each one and talk about what professional progress looks like for that phase.

The Frameworker

Just because you primarily work in a framework (or two) doesn't mean you're a Frameworker. But you might be.

Frameworks allow a Frameworker to build software products in a way similar to how IKEA allows me to build shelving units. When the work is done, did I make a shelving unit? Yes. Should I be describing myself as a Shelf Engineer? Probably not.

What is a Frameworker lacking?

Programming language knowledge

Frameworkers usually have a fairly rudimentary knowledge of the programming language that the framework is written in. What's more, they may not be aware of their lack of knowledge because frameworks are purposefully designed to provide abstractions that shield their users from many types of programming. So the Frameworker gets a lot of experience in solving a handful of problems with a limited programming toolkit.

Depth of knowledge

Frameworkers also lack depth of knowledge in the software problem they are using the framework to solve. A framework represents a specific way (implementation) of actualizing the concept of a software product. That low-level implementation is usually abstracted, and framework users are provided some straightforward ways of injecting code into the underlying implementation via base classes, decorators, and [auto-generated] templates.

Frameworkers can become adept manipulators of the framework without ever understanding underlying principles of the frameworks: design patterns, algorithms (CS or ML), asynchronous or distributed programming, and much more. Sometimes understanding these underlying concepts can offer an immediate benefit by preventing some misuse of the framework, but often one can use the framework adequately with little depth of knowledge. All of this "deeper" knowledge, however, is valuable. For one thing, it's massively transferrable — not just within the immediate domain of the particular software product, but across all sorts of programming. What's more, without this depth of knowledge, the Frameworker is not well-equipped to decide whether or not a particular framework choice can fully meet a project's requirements.

But does any of this matter?

The result of Frameworking

Frameworking has a negative effect on the Frameworker's career and on the organizations they're a part of. The Frameworker works in that limbo of writing framework code that is good enough to merge, but lacking a level of competency that will pass long-term maintainability and extensibility tests. A lot of immaturely procedural code just gets dropped into the right slots, unfamiliar problems are handled with anti-patterns, and organizational code quality drifts downward.

But from a career perspective, the Frameworker has a bleaker outlook. Coders with the same set of transferrable skills (i.e. using the framework) are being churned out en masse by about 50 new "online academies" every day. And if a Frameworker eventually lands a dream job where they'll get to do more interesting projects, they might experience a rude awakening when their knowledge gaps become evident.

Side note: they might just be made a Manager, in which case they'll probably do incredibly well.

So what if a Frameworker wants to be an Engineer? I'd suggest that they start by becoming Programmers.

The Programmer

I'm going to be using the term Programmer with a sense that is somewhat idiosyncratic to this article's premise. A Programmer is someone who lives in code. And by that vague expression, I mean two things:

  1. The Programmer is reading lots of code.
  2. The Programmer is writing lots of different sorts of code.

Let's take those in turn. And head ups, I'm going to switch back to the second person here because I believe you deserve that sort of personal touch.

The Programmer is reading lots of code

You'll definitely hear it said that reading good code is one of the best ways to learn to write good code. When you're very early on, it's a great way to learn new aspects of syntax. It's also key for learning to write code that is idiomatic to the specific programming language. You'll learn to cover edge cases and handle exceptions like a professional. But there's an even better reason to read lots of code.

The best reason to read code is just to become really good at reading code. The ability to efficiently read and understand new code is probably the most important single skill that an Engineer will have (and the Programmer is becoming an Engineer) because the technologies, languages, and libraries that make up a "core stack" are constantly changing. Sure, there will always be a need for good documentation, but there's no substitute for being able to understand code by reading it yourself. And that's especially true when moving into a new job somewhere.

Luckily, there is tons of code out there to be read, and it can be encountered while doing the second thing.

The Programmer is writing lots of different sorts of code

One of the ways to break free from a Frameworker rut is to write code that's not typical for you. That doesn't necessarily mean that you are writing a different sort of app. If you do machine learning in Python, you could have a side project implementing ML algorithms yourself. (Bonus points if you do it in C/C++) But of course it might mean that you're writing a different sort of app . Try writing a desktop GUI on top of some of the scripts you run every day, just for fun. You make web front-ends? Try writing a text-based role playing game.

The thing to be mindful of is that you don't simply want to switch from one framework to another. You're trying to learn. You want to be using your programming language at a lower level than you're used to and then creating your own layers of abstraction on top of that. You should be learning about things like I/O and sockets and event loops and buffers and hash generation and tail recursion. And as you add layers of abstraction, you should be learning about things like polymorphism and inheritance and interfaces and state machines and Design Patterns. And you should be gaining a vocabulary.

So when do I become an Engineer?

The limitations of being a Programmer

As a Programmer, you'll find that you can do things with code that would have been impossible for you as a Frameworker. But it's possible to do some remarkable things and still be a poor Engineer.

Imagine that you're building your own car from parts. But as you're assembling your car, every time you need to attach one unmoving part to another, instead of using nuts and bolts, you weld them together. You could potentially build a car that works really well. At first. But imagine something in it breaks, and it's the sort of thing that's hard to get to — you really need to remove some other parts in order to get to it. All that welding was a bad choice. Imagine that you want to upgrade just one part of the car — it would be really nice if that one part could just be popped off and the new one popped on.

I'm not saying that being a Programmer (in this sense) means that you've completely ignored good engineering principles. But the goals of this Programming phase are different from those of the Engineering phase. If Programming has been a more or less linear journey of gaining capability in certain skills, becoming an engineer will be a hard right angle. To switch metaphors, you could think of it like language development: as a programmer, you're gaining a native-speaker fluency with the languages that you work in. As an Engineer, you'll start writing academic prose.

The Engineer

So what is this mysterious shift in priorities that defines The Engineer? The Engineer plans and writes software that balances stability and change. Stability is a straightforward concept, and many Programmers provide it in their code. Planning code that can change is more elusive. Balancing the two is what makes you an Engineer.

Stability

Most people understand the main concepts of stability: The first concept is that software should do what it is supposed to do. It shouldn't be buggy. It should be reliable. The second concept is that the way the software is used should not change rapidly and frequently. That's, true in two senses: the interface should be relatively stable over time, and the interface should be consistent across the software.

I'm not going to spend more time on these, but I'll just emphasize that when I say interface, I'm referring to that broadly — it can mean a thing that you've explicitly declared (a la headers in C/C++ or Interfaces in Java/C#, etc.), it can mean a consistent set of types of data and names of objects that are interacted with, either in state or in operations (a la Python, Scala, etc.), or it could mean the graphical interface your "business" users experience. Defining a stable interface is incredibly important, not just for the sake of stability itself, but also for the sake of change.

Change

The Engineer writes software in a way that accommodates change. More precisely, the Engineer is able to anticipate the way their software will likely need to change, and writes accordingly. This ability to anticipate the future is definitely an ability that grows over time, but there are several types of changes that are universal enough to mention here.

Bugs. The Engineer assumes that every piece of software they produce has something wrong with it. In order to change (fix) the bug, code should be organized, be highly readable, and use logging. I won't go in depth here. At a minimum, the Engineer knows how to organize code into reusable blocks, whether those are functions or classes, and knows how to loosely couple those blocks of code so that bugs are as isolated as possible and require a limited number of changes.

Use Cases as Data and Behavior. I remember being on a call with a team lead who was completing an MVP targeted for a single client. I asked, "Have you designed this [software] so that it can be used for other clients by referencing a configuration file, or would it require further software development to use this for other clients?" There was a long pause on the call, and that told me everything I needed to know. When the Engineer gathers requirements for a project — even an MVP — they are constantly evaluating the required behaviors to determine if these are behaviors that are likely to change in a way that can be captured in a data layer.

An example. Client A emails you their data as Excel files which get dumped on a shared drive. Client B has an API. Client C has you connect to ftp and download CSV files. (Ignore the problems this implies in your business-tech integration for now.) There are three different behaviors here. Your software could support all three of them while depending on config files (one for each client) to identify which behavior to use. Then when Client C finally gets an API, you don't have to do software development. You just replace the ftp information in their config file with their API information.

All of this is possible because you, The Engineer, knew from the start of development that importing Excel files from the shared drive was a behavior that was likely to change. So you planned your software to deal with data importing via abstraction rather than hard-coding in a load_client_a_data_from_u_drive() function. (You laugh, but the things I've seen…) And you define a data layer to handle configuration.

Extension. In some scenarios, the use case changes significantly beyond what you were either expecting or beyond what you are willing to support. Continuing our example case, maybe you don't want to support a data loader for blob storage from every different cloud provider. If you've clearly defined the interface for loading data, you don't have to. Downstream developers can still use your software to import data from S3 by writing code that implements the interface, so long as you've left that opportunity open to them. There are a number of ways to do this and a number of concepts that can help (inheritance, injection, generics, even duck typing). The Engineer is able to write software that can be extended beyond what they can plan for.

Conclusion

So where are you in your journey? Are you ready to move beyond that entry-level resume and give some gravity to that word "engineer" that you may already have in your job title? Becoming an Engineer doesn't have to be the end, but it's a great place to go if you're ready to bust out of the frameworks.