What I learned in my first year as a Software Engineer in a startup
I started a year ago as a software engineer at Foxintelligence, a Parisian startup. I previously had 2 experiences as a project manager in IT departments so I had an idea on how to make software. But I learned many new things becoming a developer myself. Here are some.
Writing code that works is easy
Before applying to software engineering jobs for the first time I made some online trainings (mostly freecodecamp, that I recommend). I wanted to show my motivation, and what I could learn in a couple of weeks.
When I started my new job, my first challenge was to write code that works. Which means code that actually does what the specifications say. Almost everything was new. I learned about JWTs, how to connect to a database with Node.js, how Vue.js framework works and technical elements like these.
But after a couple of months writing code that works I realized that this was not the most difficult part. The hardest part is to write code that other people can understand.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Martin Fowler, 2008.
So my next challenge was to write readable and maintainable code! It was also the time when another developer got into the project I started alone. I had to explain and clean up all the parts I was the only one to understand.
Writing good code is harder
So, how do you write good code that doesn’t just work but follow good practices?
First what is good code? Some elements that help considering good code to me.
Good code is understandable by your co-workers (present and future) and your future self. Everything will be easier if more than one person at a given time can understand what the program does.
For instance, you can start by taking time to name variables, functions or errors the most clearly you can. It sounds easy but a huge amount of time will be saved if functions are well named (get, data, id should be banned). Worse than too generic, the function does something different than the name suggests. A function getSomething that also sets something inside it and thus has a side effect: not cool.
Another aspect is an effective code architecture, split by files and by function. So that people can retrieve what they are looking for. It also helps unit tests if you have functions that are well separated and can be tested in isolation.
Errors are caught
Your code is good if it doesn’t break each time there is an error. Which means there is good error handling. Indeed, errors are part of the software, it could come from many different elements. A user using the software in a way you didn’t design for. Data that don’t have the same format as you expected (via user input, API changes, etc). The number of concurrent users that you did not have in development mode or even network errors.
Even if you can’t plan for every error that will appear, you can have a more generic error handling strategy for each case it doesn’t go as expected. For example, for every route of your API, you can encapsulate all the things done in a try catch that will log the errors and send a generic error message to the client. You can’t anticipate all the errors but this way you will:
- Have a log of the error in any case
- Respond properly to the client that is waiting for an answer (without exposing your internal API errors to the client)
However you should not systematically catch every error. You may need to throw the relevant errors and not sort of swallow the real cause of the problem.
Because you won’t be here forever. Plus you want to save your time answering the same questions again and again. It’s easier to take the time to write how things work. A good start is having a readme up to date, an architecture diagram and comments in the code when something is not clear (a common advice is to explain why and not how).
Which means the product can evolve, you can add features, or enhance part of the product without breaking everything and taking too much time to refactor. It’s a subtle art of going fast to iterate (which introduces technical debt) and developing thinking about the future to be generic enough for evolution.
How to write good code?
Once we have an idea on what good code looks like, how to help people applying those principles?
To me there are 2 main mechanisms (other than the good will and the knowledge of the developers): the conception and the code reviews.
As a project manager, my first responsibility was to define the scope of the project I was working on. You need everyone aligned on the scope, because the rest will depend on that. You can’t discuss the budget, the planning, the risks, the resources of the project if you are not clear on the scope.
As a developer it’s the same: the first thing to do before even thinking about the technical conception is to define the scope of the feature. You usually do it with the product owner/manager in an agile organization.
Is this part included in the sprint or not? Can we reduce the scope to have something working (even not perfectly) at the end of the sprint? Is this part core to the feature or a nice-to-have?
Once you have a clear scope, you can start to do the technical conception. You can discuss: the name of your components, your API routes, the data format and what part of the codebase will be affected for instance. You don’t need to discuss every implementation detail or spend hours discussing a function name. But think about the overall architecture and how different parts of you application will be connected. It will reduce the subjective decisions one person will have to make and thus improve the readability and the quality of the code.
In the classic git flow, every new feature is developed on a feature branch and a pull request is made before merging the work into one of the main branches. The code review is the process of checking every change made to the code (called git diffs). People can then discuss if something is not clear, or not optimized (for the readability). It‘s hard at the beginning and it takes time but it will save way more time in the long run.
This quest for readable, clean and maintainable code is never finished. I don’t think that even the most senior developer writes perfect code 24/7. But reviewing and letting your code being reviewed greatly helps at the beginning of your career.
So what’s next? What can be more difficult than writing good code?
Working efficiently with a team of developers can be THE challenge to face
Take one good developer, you can expect him to write clean and maintainable code. Take another good developer, same thing. Now take them on the same project working together. It is not obvious that you get a good team of developers.
How do you organize? How do you split the work? How do you not overlap? How do you avoid merge conflicts? How do you avoid people blocked by another?
Those are the questions you ask yourself when you start working with a team. I have seen the team growing (up to 5 developers) and those challenges had to be addressed fast. When 5 people are working on a project, you can’t afford to lose precious time. Say you loose 1.5 hours in a meeting that is not productive with your team and that’s a day of work gone. Here are some principles we started as a team to limit the friction and stay productive.
- Divide the work up-front. During a sprint, one person will be more focus on one part of the application. It could be doing one component in the front, or implementing this API route. It’s not necessarily only front-end versus back-end but it needs to be limited to some files that other people will not touch a lot. This is best if it’s not always the same people doing the same thing. The advantages are: everyone can modify every part of the app (limiting the bus factor), better quality as every line is challenged by other developers, and people learn new and diverse things.
- Conception (again). To make sure one feature is not modifying every other file. And to anticipate the impact of every modification.
- Move fast. Everyone is encouraged to push his feature branch often and do small iterations. It limits the time one branch is living alone without rebasing on the common branch and thus limiting merge conflicts. Which also means reviewing code as soon as possible.
- Fake it until you make it. For API calls, for instance, the person working on the front-end component doesn’t need real data to start working. If the API route return fake data with the correct format (cf the conception) nobody is blocked. Same with the data shared among components through the store in a React or Vue interface.
This is an ongoing process and we regularly challenge it to improve how we work together.
Working closely with a team is not easy and it can be more challenging than writing code alone. But it is also rewarding and can make a real impact on the startup execution.
There were some challenges I’ve been through during my time as a software engineer so far. I’d like to say it’s really exciting to be part of a product team and to build something useful for our customers.
I’ll conclude by some of the topics I’m currently learning and that could be further tackled in a future article.
Developing a product from scratch is really different than having one in production. The latter includes among others:
- Application monitoring and real time dashboards (we use Kibana and Grafana)
- Understanding the environment where your app is running (containers with Docker on AWS for us)
- Performance issues and cost optimization
Another aspect of being responsible for an application running in production is that you want the least interruption of service for the people relying on it. Which is tightly linked to automated tests, especially on critical part of your application.