Oscar te Giffel®

Taste · Edition 2026-08-06

The Taste Bible.

This is my code taste, written down. Not rules, a compass. Five chapters, thirty-four verses, each one a position I arrived at by building software and getting it wrong first. Some are borrowed from people smarter than me. Most are scar tissue.

Taste evolves. What I believed five years ago about testing, abstraction, and "clean" code isn't what I believe now, and I expect the same gap five years from here. So this book is versioned, not carved. Each edition is a snapshot of what holds up today, and the revisions record what changed.

Also available as plain markdown ↗.

Chapter 1 — Foundation: Communication & Growth

1:1 Communicate to Collaborate

Communication is the foundation. Every part of software development (code reviews, architectural decisions, even how you name things) depends on clear collaboration. Give feedback well, receive it better, and build things as a team rather than in isolation.

The best technical solution means nothing if you can’t explain why it matters. Communication isn’t separate from engineering. It’s the layer everything else runs on.

1:2 Learn the Rules to Break Them Wisely

Understand your organization’s methodology so well that you know when to bend the rules. Kanban, Scrum, Tech Radar. Navigate these frameworks, but keep the bigger picture in mind.

Methodologies exist to solve problems, not to be followed blindly. The mark of maturity is knowing which rules serve your current context and which ones are getting in the way. Same idea as adapting to context: both come down to understanding the “why” behind the “what.”

1:3 Study Your Environment, Not Just Your Code

Learn the environment beyond the technical stack. Company culture, hierarchy, team dynamics. This stuff matters. Use it to adapt your approach whether you’re in a fast-moving startup or a structured enterprise.

Technical decisions don’t exist in a vacuum. The “best” architecture for a two-person startup looks nothing like the “best” architecture for a regulated enterprise. Your environment shapes what’s possible, what’s valuable, and what’s waste.

1:4 Embrace Discomfort to Grow

The problems you avoid become the gaps in your expertise. Every senior engineer has a list of “I should have learned this years ago” moments. The difference is whether you keep growing that list or start closing it.

Admit mistakes publicly. Hold yourself accountable. Seek out the uncomfortable learning opportunities. Don’t avoid problems you don’t understand. Lean into them. This applies to interpersonal skills just as much as technical ones.

1:5 Own Problems, Propose Solutions

Don’t just point at problems. Propose solutions. Instead of throwing issues “over the fence,” take ownership and work collaboratively, regardless of your role boundaries.

This mirrors the Hollywood Principle in modular design: don’t wait for someone else to fix your problems, but also don’t reach into other people’s domains uninvited. Own your boundary, propose across boundaries, collaborate at the interfaces.

1:6 Words Matter

Choose your words carefully. They shape reality within your organization. When you call something a “quick workaround” or “temporary solution,” others hear “the solution.” That casual compromise becomes the documented approach. That “just for now” architecture? It lives in production for years.

Be precise. Call workarounds what they are. Name technical debt explicitly. Say “I don’t know” instead of dressing up uncertainty as confidence. Your words don’t just describe decisions. They legitimize them and create precedents that stick around long after you’ve moved on.

Chapter 2 — Software Craft: Quality & Simplicity

2:1 Build Quality In

Quality isn’t a phase at the end of a project. It’s not a separate backlog item. It emerges from how you work daily. Embed testing, refactoring, and maintainability into your regular work instead of treating them as special initiatives that need permission.

Every commit either increases or decreases quality debt. Choose deliberately with each change whether you’re investing or borrowing against the future.

2:2 Understand Needs, Not Just Requirements

Focus on what your stakeholders actually need, not just what they ask for. Sometimes that means a proof of concept first, sometimes it means upfront design. Calibrate.

Requirements are compressed interpretations of needs, lossy by nature. Your job isn’t to implement specs perfectly. It’s to solve problems effectively. This often means questioning the requirements themselves. Same idea as questioning schemas: both reject literal interpretation in favor of understanding intent.

2:3 Simplify Relentlessly

Simple code is easier to read, test, modify, and delete. Complexity is the enemy of reliability, security, and velocity. Every abstraction, every layer, every pattern should justify its existence by reducing overall complexity, not just local complexity at the expense of global understanding.

Constantly work to reduce complexity through modular design, separation of concerns, and thoughtful testing. Use these as levers, not rigid rules.

2:4 Code Never Dies, You Have to Kill It

Grady Booch said it on The Pragmatic Engineer: “Old code never dies, you have to kill it.”

Code has inertia. Once written, it persists through momentum rather than value. Dead features linger in production. Unused functions stay “just in case.” Deprecated APIs stick around because deletion feels riskier than keeping them. But every line you keep is a line you maintain, secure, and pay to understand.

Deletion is a feature. A pull request that removes 1,000 lines of dead code is often more valuable than one that adds 1,000 lines of new features. The best codebases aren’t the ones that never made mistakes. They’re the ones that aggressively prune them.

2:5 The Code That Changes Together Stays Together

If every time you modify function A you also modify function B, they belong in the same module. Your version control history reveals the true coupling in your system. Patterns of co-change matter more than theoretical architectural boundaries.

This is cohesion made practical. High cohesion within modules, loose coupling between them. Code that changes together should live together. Code that changes independently should live apart.

2:6 Don’t Guess Where the Bottlenecks Are

You can’t tell where a program is going to spend its time. Bottlenecks show up in surprising places. Don’t put in a speed hack until you’ve proven that’s where the bottleneck actually is.

2:7 Measure Before You Optimize

Measure. Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest.

2:8 Keep It Simple When N Is Small

Fancy algorithms are slow when N is small, and N is usually small. Fancy algorithms have big constants. Until you know that N is frequently going to be big, don’t get fancy. (Even if N does get big, use the rule above first.)

2:9 Choose Simple Over Clever

Fancy algorithms are buggier than simple ones and much harder to implement correctly. Favor simple algorithms paired with simple data structures. Clever code impresses other engineers; simple code ships and works.

2:10 Let Data Drive Your Design

Choose the right data structures first, and the algorithms will often become obvious. Data structures, not algorithms, are central to programming. Well-organized data makes complex problems simple; poorly chosen structures make simple problems complex.

Chapter 3 — Data Validation

In distributed systems, validation isn’t paranoia. It’s architecture. Every boundary between systems is a point where assumptions break, schemas drift, and bad input enters.

3:1 Treat Every External Interface as a Trust Boundary

Every connection to external systems (APIs, databases, file uploads) is a point where your assumptions might break. Validate everything crossing these boundaries, no matter how confident your type declarations make you feel.

Types describe structure. Validation enforces invariants. Your type system can’t protect you from {"age": -5} or {"email": "not-an-email"}. Runtime validation catches what compile-time checking cannot.

3:2 Question Schemas, Don’t Worship Them

OpenAPI and Swagger schemas are great for documentation and client generation. But treat them as guidelines, not gospel. API providers change implementations without updating schemas more often than you’d think.

This echoes understanding needs over requirements. Schemas document intent imperfectly, just like requirements. Trust, but verify.

3:3 Validate to Defend, Not Just to Parse

Blindly trusting input shapes creates security holes. Runtime validation is a defense layer against injection attacks, malformed data, and unexpected payloads that slip past type checking.

Defense in depth means validating at every boundary, even when you “control” both sides. Your auth service might be secure today, but validation makes sure your user service doesn’t blindly trust whatever arrives claiming to be authenticated.

3:4 Invest in Validation for Better DX

Validation feels like overhead until you need to debug a production issue. Good validation turns “something broke in production” into “received invalid email format from the checkout service at 14:23:47 UTC.”

That specificity is worth its weight in gold at 3 AM.

Chapter 4 — Modular Design

4:1 Separation of Policy from Mechanism

Mechanism = how something works (the implementation) Policy = when/whether something should happen (the business rules)

Keep mechanism in the component. Push policy to the caller. The component implements capabilities. The caller, who understands the context, decides when and how to use them.

This is a core concept in OS design from papers like The Structuring of Systems Using Upcalls and fundamental to Unix philosophy. Build tools that do one thing well. Let users compose them.

4:2 Hollywood Principle: “Don’t Call Us, We’ll Call You”

Components shouldn’t reach out to query their environment. They receive everything they need through their interface.

When a component checks environment variables, global state, or external context, that’s a hidden dependency. Dependencies should be explicit in function signatures and constructors. The caller provides what’s needed; the component doesn’t go looking for it.

This is the foundation of dependency injection and inversion of control. Your component shouldn’t reach across boundaries to solve problems outside its domain. See also owning problems.

4:3 Inversion of Control

The caller controls the behavior, not the callee.

Instead of components inspecting their context to decide what to do, they accept configuration that guides their behavior. Control stays at the boundaries where context naturally exists.

IoC makes testing trivial: instead of mocking global state, you pass in test doubles through the normal interface. Your component doesn’t know if it’s in production or a test. It just does what it’s told with what it’s given.

4:4 Information Hiding

Hide implementation details, expose only necessary interfaces.

Each module encapsulates decisions that are likely to change, exposing only stable interfaces. When internals change, the change shouldn’t ripple through the system. This is Parnas’s foundational concept from his 1972 paper On the Criteria To Be Used in Decomposing Systems into Modules.

Information hiding isn’t about secrecy. It’s about managing dependencies. Hide anything that might change. Expose only what must be known.

4:5 Interface Segregation: Pass Only What’s Needed

Don’t pass entire domain objects when a component only needs one field. A function that needs an email address shouldn’t receive an entire User object. Just pass the string.

When you pass a complete domain object, you couple yourself to its entire structure. Extract the specific values at the boundary and pass only those. This is one of the SOLID principles. The fewer things a function depends on, the easier it is to understand, test, and reuse.

4:6 Law of Demeter

Only talk to your immediate friends. Don’t reach through objects.

When you write object.getA().getB().getC(), you couple yourself to the internal structure of multiple objects. If any part of that chain changes, your code breaks. The Law of Demeter isn’t about preventing all method chains. It’s about reducing coupling to distant parts of your system.

Ask objects to provide what you need directly, or extract values at the boundary. Same idea as interface segregation: both push you toward minimal, explicit dependencies.

Chapter 5 — Leadership

5:1 Lead by Crafting, Not Just Directing

Stay hands-on. Lead through the quality of your work, not the weight of your title. Deliver designs and implementations that set the standard. You earn respect by staying close to the details, not by directing from a distance.

Authority in engineering comes from demonstrated competence. Your influence grows from the quality of your contributions, not from your opinions.

5:2 Embrace Technical Risk

Take on the hard problems that others avoid. Go beyond comfortable approaches when the problem demands it. Pick up new expertise as needed. Show others what’s possible.

Senior engineers aren’t more comfortable with complexity because they’re smarter. They’re more systematic. They know when to simplify and when to accept necessary complexity. The art is knowing the difference.

5:3 Shape Culture Through Empathy

Be conscious of how your words and presence impact others, especially those with less influence. Technical leadership without emotional intelligence is just architecture documents.

Your ability to influence depends on your ability to understand others’ perspectives. Empathy is the foundation of communication.

5:4 Transform Complexity into Clarity

The most valuable skill in senior engineering isn’t writing code. It’s clarifying problems. Most technical failures trace back to solving the wrong problem or misunderstanding the real constraints. Frame each challenge in its customer and business context, then distill it to its essence.

Clarity is force multiplication.

5:5 Adapt Your Approach to the Context

Adjust your methods to fit each team, project, and product. Seek differing perspectives. Be willing to change your mind. Multiple viable solutions often exist, and sometimes the best solution is solving a different problem entirely.

This echoes learning the rules to break them. Both require reading context instead of applying rigid patterns. One-size-fits-all solutions are a sign of inexperience.

5:6 Honor the Systems You Inherit

Every “legacy system” was once a modern greenfield project built by smart people facing constraints you might not understand. Before refactoring or replacing, learn why it is the way it is. The seemingly stupid decisions were often intelligent tradeoffs given the information available at the time.

This connects to killing dead code. Respect existing code enough to understand it, but be willing to delete it when its time has passed.

5:7 Learn Continuously, Teach Broadly

Your value multiplies when you teach others. One person writing great code creates local impact. One person raising the capability of an entire team creates systemic impact.

The best engineers are also teachers, whether through code review, documentation, or mentorship.

5:8 Create Lasting Impact Beyond Code

The most important work isn’t always the most visible. Sometimes the greatest impact comes from killing a bad project, establishing a standard, or teaching a principle that propagates through the organization.

Think in systems, not features.

Chapter 6 — Concrete Taste

This chapter is a working draft — I am still editing it.

6:1 Prefer strict TypeScript, ban any and enums

I run TypeScript in strict mode on every project. any is a type-system lie — it silences the compiler and pushes errors into runtime. Enums are a leaky abstraction that compile to objects you can’t easily introspect; union types and as const objects do the same job with less magic.

If you can’t type it without any, that’s a signal the design needs work, not a reason to opt out of safety.

6:2 Named exports only

I never use default exports. Named exports make refactoring reliable — the editor finds every reference without guesswork, and the module’s public surface is explicit. Default exports encourage inconsistent naming across import sites and hide what a module actually exposes.

If a module exports one thing, name it and export it by name.

6:3 Named function components, never arrow assignments

I write React components as named function declarations. Arrow-assigned components lose their name in stack traces and devtools, which makes debugging harder. Named functions are also easier to move, split, and test in isolation.

const Foo = () => {} looks like a component but behaves like an anonymous function. function Foo() {} is unambiguous.

6:4 Type aliases over interfaces

I use type for everything. Interfaces were designed for declaration merging — a feature I almost never need and occasionally find surprising. Type aliases are more consistent, support unions and intersections naturally, and compose the same way across the entire codebase.

When the whole team uses one form, the codebase is easier to read.

6:5 Parse at the boundary, don’t validate inside

I validate at system boundaries — network responses, form inputs, environment variables — using Valibot schemas. Once data passes that boundary, the rest of the code treats it as fully typed and asks no further questions.

Scattering if (x !== undefined) guards through business logic means the parsing failed: something unknown entered the system and you’re managing it with runtime checks instead of types.

6:6 Integration tests over unit tests

I test user behaviour, not implementation details. An integration test that exercises a full slice of the system — from input to output — gives me confidence that the feature works. A unit test that checks a private function gives me confidence that the function works, which is rarely the thing I care about.

Unit tests make refactoring harder; integration tests make it safer.

6:7 Self-documenting code first, TSDoc where it counts

I name things so the code reads as a statement of intent. A comment that explains what the code does is a signal that the code should be clearer. A comment that explains why a constraint exists — a hidden invariant, an upstream quirk — is worth writing.

TSDoc belongs on public APIs where the caller can’t see the implementation. Everywhere else, the code is the documentation.

6:8 Declarative over imperative

I reach for imperative code only when the abstraction doesn’t exist yet. Declarative code describes what should happen; the reader doesn’t have to simulate execution to understand it. When I find myself writing a loop to build a list, I ask whether map, filter, or reduce says the same thing more directly.

The right abstraction makes intent visible and detail invisible.

6:9 Composition over inheritance

I build behaviour by combining small, focused pieces — not by reaching up an inheritance chain. Inheritance couples you to an ancestor you don’t fully control; composition lets you swap parts independently. Most of the time, a hook, a higher-order component, or a plain function covers what inheritance would have done with less ceremony.

I treat extending a class as a last resort, not a default pattern.

6:10 Duplication is cheaper than the wrong abstraction

I don’t reach for an abstraction until I see the same shape three times and understand exactly what varies. A premature abstraction couples unrelated callers through a shared interface neither of them chose. When that interface needs to change for one caller, it breaks the others.

Copy the code. Wait. When the pattern is clear, the right abstraction is obvious.

Revisions