Perspectives on Software Engineering

Dated Jun 6, 2022; last modified on Mon, 24 Nov 2025

On the Clean Code Movement

Good enough is good enough. The architectural choices and bugs in the implementation tend to be more impactful, so focus more on those.

Be conservative in what you consider technical debt. It should be something that slows down current/future changes, and not code that doesn’t “feel nice”. A code base that is free if technical debt is likely over-emphasizing polish over delivery.

Abstractions and indirections in the name of future-proofing tend to be wrong especially when treading new paths, where you can’t reliably predict the future. A lot of value is in knowing the design guidelines well enough, that you know when/if to deviate to better suit the current problem.

Code history can be used to determine hot spots (which tend to be updated frequently and therefore should be clean and well-abstracted) and the long tail (where we can take shortcuts without much cost).

uses CodeScene , which has hotspot detection and code-health metrics, and can be integrated into CI pipelines. It has a free edition for publicly available repositories. The pro version is €27 per active dev per month.

Code Contributions from Senior Devs

Being a senior dev takes more than just programming prowess. Other skills are communication, defining the problem, dependency management, sharing context, project management, estimation, collaborating with non-dev peers, etc..

Re: experienced devs drafting the approach, and leaving it to the junior devs to write the actual code. The senior dev might not have enough coding time, that their inconsistent coding efforts are harmful to the project. Reviewing the resulting PRs is more impactful.

The skill of experienced devs is far more helpful when it can be applied to large parts of the code-base, and hence the meetings get folks on the same page. However, companies may tend to being overly bureaucratic that communication quantity is overstated.

Working in Large Codebases

argues for consistency over trying to make your small corner of the codebase nicer than the rest of it. Large codebases have landmines that have been accounted for in the old code; you cannot split up a large established codebase without first understanding it. “Consistency for consistency’s sake” is usually a misinterpretation of “Consistency because there are reasons for the way things are done and you don’t understand those reasons well enough to diverge”. Copying for the sake of consistency only works if you actually understand what it is that you’re copying; otherwise, you’re cargo culting.

Write code that is monotonic (can only proceed in one direction) and uses immutable data structures. This helps in ruling out wide swaths of possible outcomes, e.g., with immutable data structures, the object will not change out from under your feet.

Structure your code so that pre-conditions and post-conditions are easy to conceptualize and verify. This makes it easier to reason about what your code will do in the event that it doesn’t crash.

Subdivide your code into atomic units that can maintain the invariants (things that should always be true no matter what). For example, with C++’s constructors and destructors, one can ensure that any memory an object needs only remains allocated when the object actually exists.

Build as many firewalls that prevent changes from propagating past a certain point. For example, a Nerve query can contain material fields (obtained from an API) and virtual fields (derived from material and virtual fields). The query planner computes the dependencies of query \(Q\)’s virtual fields, and adds them to a query \(Q'\) which is fed to the query executor. On receiving the results, Nerve prunes fields that weren’t in the original \(Q\).

Great Software Design Looks Too Simple

Adding rescue clauses, making sure failed API requests are retried, setting up graceful degradation, etc., are often a signal that you’re papering over flaws in a bad design. Instead, design failure modes out of existence.

Protect the hot paths. There is a 200ms-per-record inefficient catalog endpoint that brings failure modes like resource starvation, proxy timeouts, and users giving up. Move the construction code into a cron job that sticks the results in blob storage, and have the endpoint serve the blob. The cron job can’t be triggered by user actions. In the worst case, we’re serving a stale blob.

Remove components. There is.a documentation CRM that pulls docs out of different repos and stitches them into a DB. This CRM solved the problem of teams rarely writing docs. As the company grew, the sync job triggered strange git errors when on-disk state got stale, or when host ran out of memory. Solved this by removing the CRM entirely and moving docs to a central static site.

Centralize/Normalize state. The crucial parts of your system should have a single source of truth, and this is often worth taking a lot of other pain. Fixing bugs in inconsistent state is thorny because you need to fix the bug and repair all damaged records.

Use Robust Systems. The Ruby server Unicorn takes a server process that listens on a socket and handles one request at a time. To scale, you fork that server process a bunch. Standard Linux socket logic spreads requests evenly between your server processes. This design hands off most of the work to the process and Linux primitives. Process isolation is also a lot more reliable than thread isolation.

Designing Software in Your Head That Could Possibly Work

The symptom for not doing so is having a lot of technical discussions about specific details in a general plan that could not possibly work, e.g., the exact persistent-data-storage strategy to implement in a backend service that must remain stateless.

Don’t stay too high level. Suppose you’re building a comments system for a blog. “I’d put the comments in a relational DB somewhere and pull them out to put on the page,” isn’t useful for building the feature. How are the comments travelling from the user’s browser to the DB?

Don’t get too invested in the wrong specifics. Saying “Oh cool, I’ll use React” and then diving into a million micro-decisions when your first hear about the problem is too premature.

Take the most important user flow and trace (via pseudocode) the simplest possible implementation all the way through in your head. If you start with something that works, you can usually iterate into something good that works. Tracing turns up questions that are largely agnostic about the specific technologies chosen, e.g.:

  • Needs data X, but it’s only available from a slow endpoint in service Y.
  • Needs data that we don’t currently collect, e.g., static blog doesn’t collect user identity.
  • Posts are long-term cached on a CDN. Need to bust that cache for each new comment.
  • Need to account for wicked features, e.g., if running on-premise, where can the comments be stored (or disabled)?

Be open to drastically changing the plan to something else that could also work. The first rough idea you came up with is unlikely to be the best option overall.

Working Around Wicked Features

Wicked features are features that must be considered every time you build any other feature, e.g., adding a new user type. If building an image attachment feature, you must ask yourself if the new user type can add images. Even if you design your application so that you never have to do isUserTypeX(user), the fact that new capabilities must fit your user ability framework is itself a wicked feature.

Highest-paying users (usually enterprise users) love wicked features, e.g., on-premise SaaS offerings. Beware of wicked features creeping in when they don’t have to be built, e.g., brilliant engineers over-designing solutions that aren’t needed.

Misc

Aim to be in a supportive team. The feedback (e.g. in code reviews) will accelerate your learning by a lot compared to coding on your own.

Over-documenting tends to lead to staleness. Tests or other forms of automation are less likely to go out of sync with the actual code.

Right org, explosive growth phase, and a manager who saw something in me before I saw it myself. When you can complete your normal workload with 70% of your time, the extra 30% becomes your multiplier – use it to develop the perspective of someone a level above you; see more. Focus on problems, not technologies. Build goodwill early by helping others.

I’ve done fairly well in my MSFT career, but I’ve come across shooting stars that I’ll seldom see again. Most tend to switch companies within 3 years or so. Looking to figure out more of their mindset.

References

  1. 7 Absolute Truths I Unlearned as Junior Developer. Monica Lent. monicalent.com . Jun 3, 2019. Accessed Jun 6, 2022.
  2. Absolute truths I unlearned as junior developer (2019) | Hacker News. news.ycombinator.com . Accessed Jun 6, 2022.
  3. Why I Write Dirty Code: Code quality in context. Adam Tornhill. www.adamtornhill.com . 2019. Accessed Jun 9, 2022.
  4. New Grad to Staff at Meta in 3 years. Ryan Peterman; Evan King. www.developing.dev . news.ycombinator.com . Accessed Dec 24, 2024.
  5. Mistakes engineers make in large established codebases | sean goedecke. Sean Goedecke. www.seangoedecke.com . Accessed Jan 9, 2025.
  6. Mistakes engineers make in large established codebases | Hacker News. news.ycombinator.com . Accessed Jan 9, 2025.
  7. To be a better programmer, write little proofs in your head. Matthew Prast. the-nerve-blog.ghost.io . Jul 14, 2025. Accessed Jul 20, 2025.
  8. Great software design looks underwhelming. Sean Goedecke. www.seangoedecke.com . Mar 7, 2025. Accessed Nov 24, 2025.
  9. Designing software that could possibly work. Sean Goedecke. www.seangoedecke.com . Apr 25, 2025. Accessed Nov 24, 2025.
  10. Wicked features. Sean Goedecke. www.seangoedecke.com . Apr 12, 2025. Accessed Nov 24, 2025.