10 Lessons I Learned in 6 Years of Software Engineering
Lexis Solutions
Posted on November 23, 2022
I wrote my first line of code that went to production six years ago. It was the summer after my Computer Science bachelor's began, and I was eager to put all of the knowledge I got up to that moment to good use. I developed an app, published it to the Play Store, and quickly reached users who still use it today.
Over the years, with many projects and the privilege of working with gifted engineers, some realizations occurred to me that helped me become a better professional. To celebrate the occasion, I compiled a non-exhaustive list of a-ha moments I had, to depict the path and learnings of a (WIP) software engineer.
Standardize code
It came to me as a revelation when I realized the best way to write code is to try to do it as a machine would. Define a set of rules and stick to them. Use style guides and tools that enforce them as much as possible. ESLint & Prettier have been an absolute must in the projects I have started. While those are great for syntactic "dumb" conventions, higher-level conventions can be extracted to the README - followed and exercised with every PR by the team members. Use the same "tropes," "patterns," or ways of doing things in similar contexts. And while one might think this is only good for teams, a single-person project can quickly deteriorate and become a mess. As there is nobody to stop you from writing however you like, developers are not consistent in their style and prone to mixing and matching styles and ideas. The advantages of trying to write code like a machine are manifold.
First, this drastically reduces the number of tiny decisions you have to make, e.g.
First, this drastically reduces the number of tiny decisions you have to make, e.g.
- Do I name this const object
Colors
orCOLORS
? - Do I define my
useCallbacks
first or myuseMemos
? - Do I name this endpoint
allUsers
orusers
? - Do I write if
(foo) return;
or if(foo) { return; }
- Do I use a named function or an arrow function?
These tiny decisions eat away from your energy to focus on what's genuinely human about programming - the creative part.
Second, this makes refactoring easier. Find and replace works best if the codebase has a consistent style.
Third, this reduces the friction of switching projects in an organization if the projects use the same (or similar) conventions.
Fourth, there are fewer merge conflicts - saving you time otherwise spent on solving those. In the last years, my teams have used tools to order the imports in files deterministically and order the props of a React component. Not a single merge conflict regarding those has risen since then.
Chase simplicity despite complexity
I notice, especially at the beginning of their careers, developers solve problems bottom-up, solving parts of the big problem and then trying to piece everything together. Doing so often results in bloated and complex code, which is prone to becoming spaghetti and is hard to maintain. In my first project, I certainly remember how proud I was when I wrote a Java file with 1.2K lines! What I discovered as a better method of going from spec to implementation is top-down, not worrying too much about detail, and trying to get the bigger picture right. In this feature which are the main components? How do they interact? Are there dependencies? Of course, things will not be apparent right away. What works for me is a cyclic process:
- come up with a big picture
- go into detail about a component
- inevitably things come up that require changes to the big picture
- update your existing big picture; in most cases, this will bring complexity
- can it be simplified and still do the same thing?
- repeat 2-5 until ready for implementation
Requirements push the code towards complexity. As developers, we must do our best to counteract that force by trying to solve those complex problems with the (possibly) most straightforward solutions. After all, the best code is less code.
Don't overdo abstraction
Talking about less code, taking that idea to the extreme can also be counterproductive. By all means, I am guilty of building abstractions over classes that are vaguely connected only to extract a 3-line class method. I have also written some shared packages that became more of a headache to maintain, only to follow the famously evangelized DRY principle in countless programming books. The bottom line is that sometimes abstraction is not worth it. Before building an abstraction, weigh the pros and cons of maintaining another software artifact versus having multiple instances of it in existing artifacts. Ask the question, how similar are what you are trying to abstract away in essence, in pure words, as an entity, not code? This lesson was a revelation from no other than Dan Abramov (not the creator of React) from a 2019 talk, "The Wet Codebase," which I highly recommend.
Read the documentation (no, for real)
An inspiration for countless memes, the documentation is often ignored when you hop on the hottest new dev tool/technology. Not only does it provide a structured way to comprehend the technology, but it also outlines edge cases that you might not even be aware you are getting into. A while back, my team employed Prisma for an ORM to large-scale project. It was using GraphQL in conjunction, and we had missed reading this little guide on query optimization and the n+1 problem, which is precisely the problem we shipped to production unknowingly. After some performance issues, a team member finally spotted the post, which fixed our problem.
The bottom line is that the documentation provides not only what a tool can do but also its limitations, what to watch out for, what's the roadmap, how well maintained it is, and who the people behind it are. Avoid treating it as a flashy advertisement but as a receipt that describes how to use it, its side effects, and what can go wrong.
Use and build tools to consolidate the development process complexity
As a project evolves, it gets more complex to deal with during development. Maybe you have microservices and need to start all the services one by one, or perhaps you extract translation keys before starting your front-end. To minimize the learning curve, any task or process newly onboarded developers have to learn is better consolidated in shell scripts. These scripts rarely change; keeping them in the git repository will make your workflow faster and shorten the time for new hires to start being productive. If you see an opportunity to create tooling that doesn't exist, go for it. It could be a simple shell alias as alias ys=yarn start, a VSCode plugin, a dev dependency, an integration, you name it; even saving a few keystrokes counts. Chances are, your team or even external communities of developers could also benefit from it.
Notice when you are stuck
There is nothing heroic about trying to solve a problem on your own at any cost. Give your best attempt, maybe twice, but learn to recognize the moment when you should seek to speak to someone. And notice I don't say seek help. I have seen some of the most significant breakthroughs happen while simply explaining where someone is at with a problem. It doesn't have to be your engineering lead or CTO; it could be your peer or even someone with less experience than you. A simple "hey, does anyone have some time to check out this problem with me" on Slack can go a long way. Is nobody online? Come back to the task after a walk, run, or a good night's sleep. Almost any senior developer I know does this. Pushing hours and days alone with no progress on a task is selfish, as it detracts from your team's goals and burns money for no justifiable reason for the stakeholders.
Present your work as early as possible
I have spent all my professional career in startups or startup-like environments - fast-paced and flat organizations where the top priority is to deliver value in very short cycles. One of the more recent rituals we stumbled upon is what we call "local demos." It is essentially a screen share session between two or more developers where the feature's author presents the work on their local development environment before opening a PR request. This ritual has proven to be a great way of catching code and requirement issues even before they are in the form of a PR request. Viewing the code and the result alongside the author gives the future PR reviewer a better idea of what to look at when the PR is ready to be merged. We even do these sessions with clients and QAs, allowing them to peek at the progress. Instead of a bug going through a PR, QA testing, or potentially a staging environment, it's detected right there and then. So far, many bugs have been eliminated in the earliest stage possible, enabling us to produce a better product in shorter cycles.
Don't write code for the sake of writing code; it's always about the client
The internet somewhat idolizes this hardcore hacker dev persona - always writing the cleanest code, solving problems with possibly the most efficient algorithms, sunglasses in a dark room, Vim? As romantic as it sounds, the expectation of being this brilliant engineer working on exciting and challenging problems all the time is unrealistic. In a commercial setup, what matters is building a product that people want and use, given deadline and budget limitations. It is a cherishable trait to put yourself in the stakeholders' shoes and have their concerns in mind when doing the actual engineering instead of solely focusing on solving problems in the most elegant way possible. More often than not, a working solution now is better than the best solution tomorrow. Of course, this is no excuse to put in the minimal amount of work and produce the "minimum non-sucking product." The best engineers create robust, sustainable, and scalable solutions given all the constraints.
Production is nothing like development
It took three months for me to publish my first app. I had put so much energy and thought into every detail; it looked great and worked perfectly on my emulator and phone. I was so excited to share it with the world. Then I onboarded the first users. A few days in, complaints were raining - "It's too slow!", "It's crashing," "It sucks!" My phone was ringing every day with users and their bug reports. This period gets to you when you have never published anything to production, and it is easy to feel inadequate as a developer. The truth is, nothing prepares a developer early in their career for the utter chaos of an actual mass of people using your product. It's an entirely different set of problems than the programming itself. Deployment, availability, backups, scaling, and performance are problems that reveal themselves only when you hit production usage. In big companies, those roles are separate, so not all programmers are concerned with deployment & reliability. But I would argue that working on software from inception to end user has given me good insights I still use daily. Plus, it's always fun to watch your baby fly (or flop!).
The good thing is that these lessons never end; they change, they become obsolete, or plain wrong. The field of software engineering is a gift that keeps on giving. Every day, millions of people are hard at work imagining the world's future using code, and I sure am looking forward to what we will build in the next six years and the lessons that come along with it.
Bilyal Mestanov - Co-founder at Lexis Solutions
Posted on November 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.