Pular para o conteúdo principal
Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Software Engineering Craft

Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

API & Interface Design in the Small

An API here is the local kind: a function signature, an interface, a library’s exported surface. The same rules recursively govern HTTP APIs and RPC services, but the small-API craft is the foundation — most “distributed system is hard” pain is, on inspection, the local API being badly shaped first.

This topic is the design moves that make APIs survive their callers across years of caller churn.

Designing a Function Signature

A signature is a contract written once and read thousands of times. Three rules compound:

  1. Few positional parameters. Beyond three positional args, every callsite is wrong on the first try. Move to an options object for n > 3 or when any param is optional.
  2. Required first, optional last. Surprising order is the most common call bug. Required → optional → defaults.
  3. Booleans are usually a smell. setMode(true) reads as tax-form code; enableFeature() and disableFeature() read as intent. Prefer enums or two functions when the boolean means a mode switch.
Bad:   redirectTo(user, targetUrl, true, false)   // what are 1, 0?
OK:    redirectTo(user, { target: url, preserveHistory: true, force: false })
Best:  redirectTo(user, { target: url })           // when defaults are right for 95% of calls

Return Shapes

A function returns either a value or a disposition (success or failure). The shape of the return signals to the caller how to handle it.

Return shapeWhen it’s rightFailure mode
Value or nullA truly absent casenull propagates; callers forget the check
Value or throwTruly exceptional failureCatch-all swallows in caller code
Result<T, E>Two expected outcomesSome languages lack native support
Optional / nullable monadThe pipeline semanticsForces downstream to acknowledge absence

The rule that ages well: prefer Result/error over null over throw. The caller cannot ignore a Result — the compiler makes them handle both cases. null is forgettable. Exceptions are sometimes forgotten and sometimes too loud (the function aborts one caller’s unrelated test).

Naming That Scales

A name is the documentation that ships with the function. Three tests:

  • The one-call test: from any callsite, does the function’s purpose mean alone? fetchUser(id) should not need a comment.
  • The signal test: does the name disambiguate from siblings? getUser and findUser should differ — one throws and one returns null? Name them requireUser and findUser.
  • The honest test: validateX should not also normalise X; if it does, rename to normaliseX. Honest verbs enable honest naming.

The cost of a bad name is every future reader. Spend the extra minute.

The Pit of Success

An API is a pit of success if the easiest way to use it is the correct way. The opposite — a pit of failure — makes the wrong path shorter than the right one.

Pit of failure:
  Map<String, Object> config = new HashMap<>();
  config.put("timeout", 5000);          // integer seconds or milliseconds?
  config.put("secure", "yes");          // any string works, only "yes" means yes

Pit of success:
  client.withTimeout(Duration.ofSeconds(5))
        .withSecureFlag(true)            // strong types, named methods

Three properties create the pit:

  1. Strong types — a misspelt config key is a compile error, not a silent runtime miss.
  2. Builder style — every method returns the type being configured, so completion guides the caller.
  3. Sensible defaults — the 95% case is one line; the 5% case is clearer than before.

Versioning Internals

Internal APIs do need versioning — any function called from more than one module is an internal API. The minimum:

  • Semantic versioning at the package level: MAJOR.MINOR.PATCH. A breaking change is always a major bump; the major number is the compatibility contract.
  • Deprecation cycle for any breaking change:
    1. Mark old symbol @deprecated with a migration note pointing at the replacement.
    2. Add the replacement alongside, enabled by default for new callers.
    3. After one release cycle, route remaining callers to the new symbol.
    4. After another release cycle, remove the old symbol.

Skipping the cycle is the cause of every “we upgraded and everything broke” story.

Deprecation as a Process

Killing an API is harder than writing it. The process:

StageAudience stateDeveloper cost
AnnounceAll current callersWrite the migration doc; ship the replacement
CoexistNew code uses replacement; old still worksMaintain both during transition
WarnAll callers see a deprecation logSome will migrate; some will not
Forbid (if needed)Old symbol raises or is gatedYou now own the rollout cost
RemoveOld symbol goneMost callers migrated; breakage audits the rest

The grace period depends on your audience. An internal library can move in a quarter. A public open-source library may need years. The discipline is: never break silently, never break without a path, never break without a date.

Practice Trajectory

  1. Pick a function with n > 3 positional params; refactor to an options object. Compare callsites before and after.
  2. Find any function returning null or nullable that callers forget to check; rewrite its return as Result<T, E> (or your language’s equivalent) and watch the compiler force handling.
  3. Audit a boolean parameter on an exported function: replace with two named methods or an enum, and document which one was right for which callsite.
  4. Add a deprecation cycle to one symbol in your own code: announce in a tag, coexist in the next, warn in the next.
  5. Read an unfamiliar library API you use; identify one pit-of-failure in it. Propose — not in a PR, on paper — the smallest shape change that would turn it into a pit of success.

When It’s the Right Tool

SituationTakeaway
Signatures growing more optional paramsRefactor to options object before positional order surprise bites
Functions returning nullable that callers ignoreMove to Result so the compiler enforces handling
Naming indistinguishable siblings (get vs find)Rename to express the disposition, not just the action
Breaking change being proposedSchedule the deprecation cycle; never break silently
New API being designedMake the right path the easy path: pit of success, not pit of failure