Essential Tips for Junior Developers: What Thousands of Code Reviews Taught Me
Tags: webdev, python, programming, beginners
Published: 2024-06-25 11:54:19

## Background 

Over the last year or so, I reviewed over 2,000 merge requests from almost 50 engineers, many of whom were junior engineers just starting their careers. After a while, I started to notice a pattern of frequently occurring issues in the code reviews. With some GPT-assisted analysis, I compiled the following set of tips for junior developers to help them write better code. This goes beyond the basics like exception handling, documentation, or unit tests—those are important, but I believe everyone understands that to some extent. Below are the often underestimated aspects.

## Tip 1: The IDE is your best friend!

Many developers do not fully utilize the tools available in modern IDEs, from auto-formatters to linters that can catch stylistic issues and even some errors. This is especially important for interpreted languages like Python, where there is no compiler to catch errors beforehand. Setting up tools like Pylint, Flake8, and Black can save you from many runtime exceptions and make your code more consistent.

Configuring your IDE properly can significantly improve productivity. Use hotkeys for faster search and navigation, and take advantage of tools like port-forwarding when connecting to remote SSH systems. Stack-specific extensions, such as those for Django templates or YAML files, can also make development much easier and faster.

## Tip 2: Avoid nesting whenver possible

From a code structure and complexity point of view, nesting makes the code hard to read and reason about. Deep indentation requires keeping track of more context, which can be mentally taxing. Using early returns in functions and early continues in loops can dramatically simplify your code.

```
# Example: Avoiding nesting by using early return and continue
def process_items(items):
    if not items: return 

    for item in items:
        if not item.is_valid():
            continue
        # Process valid items
        process(item)
```

## Tip 3: No DB queries inside loops at all costs

One of the biggest performance pitfalls is the overhead of database queries within loops. Each query adds IO latency, and ORMs can hide the fact that certain property accesses may result in multiple queries per iteration. This can severely slow down your application and database server.

Leverage joins, prefetching of related fields, or other ORM features to minimize queries. Use logging to track which queries are being executed unexpectedly often. Understanding your ORM and the SQL it generates is a valuable skill for any project.

```
# Example: Using prefetch_related to avoid queries in a loop
# Django ORM example
orders = Order.objects.prefetch_related('user')
for order in orders:
    process(order.user)
```

## Tip 4: Understand data access patterns and choose appropriate data structures

When implementing most features, the lazy option is to use a `List` or `Dictionary` for everything. Many junior developers fall prey to this despite their better judgement and despite knowing all the various data structures from school. 

Different data access patterns require different data structures. Using a Set instead of a List can dramatically improve performance when making many existence checks in a loop. Similarly, using a Dictionary instead of .FirstOrDefault() on a list in .NET can significantly improve performance.

More advanced data structures _should_ be considered too when applicable. For example, one of the merge requests I was reviewing required lookup based on 4-5 fields. The caveat was that one of those fields was a numeric field that needed to be compared using range checks. Naturally built-in data structures did not help much but a custom binary search based method was able to dramatically improve performance.

Premature optimization is discouraged, but a basic understanding of performance implications can guide better coding practices. Be aware of the time complexity of algorithms and the memory footprint of your data structures. Profiling tools can help identify bottlenecks in your application. Optimize only after identifying real performance issues, and make data-driven decisions based on profiling results.

## Tip 5: Become an expert at searching through code

In a large codebase, someone has likely implemented similar functionality to what you need. By becoming proficient in searching through code, you can find reusable chunks of code or helper methods that have already been reviewed, tested, and optimized. This not only saves time but also ensures consistency across the codebase.

Effective code search skills involve knowing how to use your IDE's search functionality, understanding the project's structure, and being familiar with naming conventions used by your team. Additionally, exploring the version history and previous implementations can provide valuable insights into why certain decisions were made.

Duplication not only makes the codebase larger and harder to maintain but also increases the risk of inconsistencies and bugs. By leveraging existing machinery, you can eliminate redundancies and build on a solid foundation. This approach encourages collaboration and knowledge sharing within the team, as you become more familiar with your colleagues' work and the overall project.

## Tip 6: Discipline to make small, cohesive merge requests

Junior developers often try to make massive merge requests to ensure everything works before sending it for review. This makes it challenging for reviewers to thoroughly examine the code and provide constructive feedback. The longer the merge request, the fewer comments you are likely to receive, as reviewers might be overwhelmed by the sheer volume of changes.

Smaller merge requests offer several benefits. They make it easier to write unit tests and ensure each change is well-tested. They also make large tasks more manageable by breaking them into meaningful steps. This approach promotes adherence to the single responsibility principle, where each class or function has a clear, focused purpose.

To achieve this, plan your work in advance and identify logical checkpoints where you can split your changes. Write self-documenting code by using meaningful variable names, function names, and class names that clearly convey their purpose and usage. This makes your code easier to understand without extensive comments, improving readability and maintainability.


## Tip 7: Read through a lot of code 

Reading through your own code and others' code is a useful exercise. Often, even reading your own merge request a day after submitting it can reveal hard-to-understand sections or obvious mistakes. This practice helps you gain a fresh perspective on your work and identify areas for improvement.

Engage in peer code reviews as much as possible. Reviewing code is a critical part of the development process, and it provides an opportunity for junior developers to learn from their peers. Focus on both the correctness and readability of the code. Provide constructive feedback and suggest improvements, while also being open to receiving feedback on your own code.

Through code reviews, you can learn different approaches and techniques to solve problems. This exposure helps you understand the rationale behind certain design choices and fosters a deeper understanding of the codebase. Over time, you will develop a keen eye for spotting potential issues and areas for optimization.

## Tip 8: Master version control systems and the terminal in general

Understanding and effectively using version control systems, particularly Git, is crucial. This involves more than just knowing how to commit, push, and pull changes. It includes understanding branching strategies, handling merge conflicts, and writing meaningful commit messages that provide context for your changes.

Familiarize yourself with advanced Git commands and workflows, such as rebasing, cherry-picking, and bisecting to identify problematic commits. Learn how to use Git's history and log features to trace changes and understand the evolution of the codebase.

Additionally, becoming comfortable with the terminal can greatly enhance your efficiency. Tools like screen, grep, sed, and awk are extremely useful for various tasks, such as searching through logs, editing files, and automating repetitive tasks. Embrace the terminal as a powerful tool that complements your development workflow.

## Tip 9: Prioritize security from the start

Security should be a fundamental consideration in your development process. Familiarize yourself with common security vulnerabilities, such as SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF). Implement security best practices, such as input validation, encryption, and secure authentication methods.

Regularly review and update your code to address potential security issues. Ensure that all routes have appropriate authorization from the start, and define certain invariants that all queries should follow. Always validate data on the backend, as client-side validation alone is insufficient.

Understand the principles of secure coding and apply them consistently. For example, avoid storing sensitive information in JWTs because anyone can read them without needing to decrypt the data. Use environment variables to store sensitive configuration details and avoid hardcoding them in your source code.

## Tip 10: Become very comfortable with the entire request lifecycle

When implementing a new feature, it is easy to overlook intermediate steps like the reverse proxy (e.g., nginx), middlewares, decorators, and filters. Understanding the request lifecycle helps prevent mistakes ranging from security vulnerabilities to logical errors. Knowing how data is transferred from start to finish can help you appreciate why things are the way they are.

For instance, understanding how middlewares can modify requests and responses can help you implement features such as logging, authentication, and error handling more effectively. Similarly, knowing how reverse proxies handle requests can help you optimize performance and ensure the security of your application.

Be aware of which parts of the state are ephemeral (i.e., die with the request) and which are stateful. For example, global or static variables may have a different lifespan than the request, requiring careful handling to avoid unintended side effects. This knowledge is crucial for debugging issues related to state management and concurrency.

## Conclusion

By incorporating these tips into your development routine, you can significantly enhance your coding skills, making your code more efficient, readable, and secure. Remember, continuous learning and adaptation are key in the ever-evolving field of software development. Embrace these best practices, and you'll find yourself writing better code and contributing more effectively to your team.