Experience Nuggets

These are bits and pieces of lessons I've picked up over the years. They are not revolutionary, but they are good reminders.

Unapologetically inspired by The Grug Brained Developer, continuously updated.


Philosophical nuggets

1. Recognize being a victim of your own experience

Each of our experiences is different and unique. Thus, my opinions are different than a FAANG engineer’s. The market, the work, and how you get jobs also differ between the US, EU and around the world. A lot of people believe their experience is universal - it’s not. Social media connects us, but it’s important to understand the differences between our perspectives, mindsets and value systems. When reading other engineer’s yappings, it’s important to be aware of their context - it may not be the same as yours.

2. Opinions are not facts, seek different perspectives

Software development is complex and there are many opinions on how to do it right. Don’t confuse opinions with facts, take everything with a grain of salt, it’s not all black and white. Avoid dogmas, no matter how popular the author of an idea/post/book is. Consume advice critically, be able to disagree and form your own opinions (this comes with time and experience, so don’t rush it either). Everything can and should be challenged from time to time.

3. Product Mindset > Engineering-for-Engineering's-Sake

Writing the cleanest, smartest, most beautiful software is fun. If we get bogged down and don’t ship, then it’s also a waste of time. Product is king: the goal is to ship software and make our future selves/teammates’ lives as easy as possible to change what we ship. This absolutely does require knowledge of how to design & architect code.

4. Good enough is (almost always) enough

Good enough software today is far better than a perfect system next year (which will never be shipped). Do not develop a tunnel vision towards one implementation. Think of alternatives, different approaches and talk with people - a simpler solution that is good enough is often just enough.

5. Yesterday's pride is today's problem

We learn and learn - when looking back at code I wrote a week, month or year ago, its embarrassing. We will always notice opportunities, patterns, and techniques at hindsight, which our younger selves were ignorant about. This should never change (when it does, it means we’ve stopped improving).


Technical nuggets

1. Modular monoliths > microservices

Microservices are more often than not overkill. A modular monolith with well-separated bounded contexts is an excellent compromise between microservices and a monolith.

2. ETC > DRY/KISS/YAGNI/SOLID/...

ETC, as in ‘Easy To Change’. DRY, KISS, YAGNI, and SOLID all have the same underlying idea to some extent: to have the codebase easily changeable in the future, be it for new features or bug fixes.

3. Locality of Behavior > Abstractions

This has been said a lot by now. Reduce the cognitive load, by keeping cohesion high and coupling low. Long functions and deep modules are okay. Keep the interface shallow & short, and have the complexity encapsulated locally as an implementation-detail. Picked up from A Philosophy of Software Design.

4. Domain Driven Design is Object Oriented Programming done right

The principles of Domain Driven Design are really useful when the environment calls for it (eg. you are working on a larger scale, complex application with a lot of moving parts - business/domain logic). Encapsulate the domain logic in entities, not across services/stored procedures/etc.

5. Functional core, errors as values

It’s perfectly valid to combine imperative and declarative paradigms. Have the core domain functional, even if it’s a state machine and not a set of pure functions. Combine this with explicit error handling by using the error-as-value pattern (Result/Option types).

6. Encapsulate volatility

I love Domain Driven Design, but I’ve been convinced that it’s a good idea to encapsulate a unit of code that you know is volatile, even if it spans across contexts. DDD is flexible and modelling is opinion-based - have common sense, and be sensible. Picked up from Righting Software.

7. Tested domain over TDD

I love unit tests where they make sense (eg. if you are doing DDD, unit testing the core domain is trivial - no need for mocking, and you are covering the moving parts). No matter how you end up with a tested domain, writing tests before or after is just a matter of preference. I don’t think TDD is as big of a deal as people make it out to be.

8. Feature flags > git branches (Deploy != Release)

Trunk-based development with feature flags is the way to iterate quickly. Do not write a feature-flag-provider yourself, use a SaaS to be able to soft-release features to users in production. Keep your feature branches down to a minimum. Deploy often to prod, and release via toggling feature flags.

9. Keep the number of environments to a minimum

Ties to the previous point. Be cautious about testing in staging/UAT, production is always going to be oh-so-different and bite you. Test on prod with feature flags (eg. soft release features to power users) & lose unnecessary environments. While the idea of testing on staging is sensible, it rarely works for stuff that is complex enough to require thorough testing.

10. Use-Case-Coverage > Code-Coverage for tests

It’s illogical to aim for 90-100% test coverage if we’re testing use cases that only the test cares about. Look at the bigger picture & have common sense: are we scared to do a production deploy, because we are unsure about our change? Congratulations, there’s your indicator that needs tests.

11. Learn new languages, but be picky

If you are a Java developer, learning OCaml or Rust will probably benefit you more than learning C# (if you are learning for the sake of improving, and not for the sake of employability). Look for languages that challenge your mindset, and have different conventions and paradigms.