The rules to break and how to break them.

Note: The examples in this book are written in C#.

  • To the streets
    • One thing is clear in the streets: your throughput is what matters most.
    • A good design pattern or a good algorithm can increase your throughput. If it doesn’t help your throughput, it’s not useful.
    • A street coder ideally possesses these qualities:
      • Questioning
      • Results-driven (aka, “results-oriented” in HR-speak)
      • High-throughput
      • Embracing complexity and ambiguity
    • We think that there is a technology out there that can increase our productivity by orders of magnitude. There isn’t.
  • Practical theory
    • Big-O notation isn’t only used for measuring increase in computational steps, aka time complexity, but it’s also used for measuring the increase in memory usage, which is called space complexity.
    • An algorithm might be fast, but it could have polynomial growth in memory.
    • You need to be familiar with how Big-O notation explains the growth in an algorithm’s execution speed and memory usage so you can make informed decisions when choosing which data structure and algorithm to use.
    • How data is laid out can make things faster or more efficient, or the opposite.
    • Failing early is one of the best practices in development. Data types are one of the earliest defenses against development friction in coding.
    • Custom data types are powerful because they can explain your design better than primitive types and can help you avoid repetitive validation, and therefore, bugs.
    • Nullability checks help you think about your intentions for the code you’re writing. You will have a clearer idea whether that value is truly optional or whether it doesn’t need to be optional at all.
    • Existing types can use more efficient storage for free.
  • Useful anti-patterns
    • Dependencies caused by code reuse are okay as long as you can keep the dependency chain organized and compartmentalized.
    • The first habit you must adopt is to avoid violating abstraction boundaries for dependencies.
    • If changing code is risky, writing it from scratch must be orders of magnitude riskier.
    • If you feel stuck developing something new, write it from scratch. I’d say don’t even save the previous copy of your work, but you might want to in case you’re not really sure if you can do it again really quickly.
    • There are ways to deal with code rigidity, and one of them is to keep the code churning so it doesn’t solidify—as far as the analogy goes.
    • Make it a regular habit to upgrade libraries. This will break your code occasionally, and thanks to that, you’ll find out which part of your code is more fragile, and you can add more test coverage.
    • You shouldn’t hesitate to improve working code. Improvements can be small: adding some necessary comments, removing some unnecessary ones, naming things better.
    • Using composition over inheritance can require writing substantially more code because you might need to define dependencies with interfaces instead of concrete references, but it would also free the code from dependencies.
  • Tasty testing
    • You need to find a sweet spot that costs you the least and poses the least risk.
    • Writing tests after you have a good prototype works as a recap exercise for your design. You go over the whole code once again with tests in mind.
    • Code for each test should describe the input and the expected output of a function by how it’s written and how it’s named.
    • Every time you fix a bug, adding a test for it will ensure you won’t have to deal with that bug again, ever.
    • Bugs don’t appear homogeneously. Not every code line has the same probability of producing a bug. It’s more likely to find bugs in more commonly used code and code with high churn.
    • Avoid trying to pass state in parameters and leverage functions as much as possible.
  • Rewarding refactoring
    • Refactoring is the art of changing the structure of the code.
    • The best way to work with a large refactor is to split your code into semantically distinct components.
    • How do you make the existing code and the new code with the same component at the same time? You move it into a separate project.
    • Planning your work with multiple integration steps is the most feasible way to perform a large refactor.
    • You can abstract away a dependency that you don’t want to deal with by creating an interface for it and receiving its implementation in a constructor. That technique is called dependency injection.
    • The secret to reliable refactoring is testing. If you can make sure your code has good test coverage, you can have much more freedom in changing it.
  • Security by scrutiny
    • https:// letsencrypt.org
    • Security is complex. You should never write your own implementation of a security mechanism, be it hashing, encryption, or throttling.
    • The safest way to solve an SQL injection problem is to use parameterized queries. Instead of modifying the query string itself, you pass down an additional list of parameters, and the underlying DB provider handles it all.
    • Never use single iteration hash functions, even if they are cryptographically secure, such as SHA2, SHA3, and, God forbid, never MD5 or SHA1 because they have long been broken.
    • There are newer hash algorithms like bcrypt, scrypt, and Argon2 that are also resistant to GPU or ASIC-based attacks.
  • Opinionated optimization
    • The second-best way to reduce the number of instructions executed is to choose a faster algorithm. The best way obviously is to delete the code entirely.
    • When writing code in nested loops, we underestimate the effects of multiplication.
    • Use asynchronous programming to run code and I/O operations in parallel without wasting threads.
  • Palatable scalability
    • From a systems perspective, scalability means the ability to make a system faster by throwing more hardware at it.
    • From a programming perspective, a scalable code can keep its responsiveness constant in the face of increasing demand.
    • Don’t use locks because they magically make the code they surround thread-safe. Understand how locks work, and be explicit about what you’re doing.
    • When you only have a single connection to a database, you can’t run parallel queries against the database.
    • A monolith is the natural next step to switch to from your local prototype, too. Go with the flow and consider adopting a microservice architecture only when its benefits outweigh its drawbacks.
  • Living with bugs
    • It’s impossible to have a bug-free program.
    • Any bug that has both low priority and low severity can be considered a won’t fix and can be taken off your radar.
    • The first rule of exception handling is, you don’t catch an exception.
    • Catch an exception only when it’s expected.

Leave a comment