Categorias
Programming Technology

The Principle Is Not the Mechanism: ISP, DIP, and Language Structures in Practice

A small refactor made me think: how many times do we mix a principle with mechanisms? For example, Interface Segregation and Dependency Inversion are many times seen naturally in OOP, but they are principles and they survive a change of paradigm. TypeScript is this interesting case: it supports both OOP and functional styles in the same codebase, and both can preserve the I and D from SOLID. This article separates the principles from the mechanisms that carry them, maps how OOP and functional TypeScript preserve the same design responsibilities. And it proposes yet another comparison of things that look tied together, but are separate concepts in the language structures underneath: where structural typing and duck typing share a philosophy but diverge in practice.

Contents

  1. The in-memory repository that made the mapping visible
  2. Preserve the responsibilities, change the mechanisms
  3. Map responsibilities, not syntax
  4. Service factories, closures, and composition
  5. Structural typing: the conformance mechanism
  6. Testing: where duck typing walks back in
  7. Untangling the five concepts
  8. The language field guide
  9. A good way to use them
  10. The honest counterpoint
  11. Checklist
  12. Recap

Obs.: Check the Glossary at the end if you need clarification on concepts I am using in the article.

The in-memory repository that made the mapping visible

In a production TypeScript API I work on, the persistence design was still evolving while the Use Cases needed to move forward. We deliberately introduced an in-memory implementation of the domain contract:

// infrastructure/repository/in-memory.user.repository.ts
import { randomUUID } from 'node:crypto';
import type { NewUser, User, UserRepository } from '../../domain/user';

const usersByDocument = new Map<string, User>();

async function findByDocument(document: string): Promise<User | null> {
  return usersByDocument.get(document) ?? null;
}

async function create(data: NewUser): Promise<User> {
  const user: User = { id: randomUUID(), ...data, createdAt: new Date() };
  usersByDocument.set(data.document, user);
  return user;
}

export const inMemoryUserRepository: UserRepository = { findByDocument, create };

The services that depended on UserRepository received it as a parameter:

export async function register(
  input: RegisterInput,
  users: UserRepository,
): Promise<User> {
  const existing = await users.findByDocument(input.document);
  // ...
  return users.create(input);
}

At the call site, the temporary seam was explicit:

await register(input, inMemoryUserRepository);

That parameter was deliberate scaffolding. It allowed the Use Cases to progress against the contract while making the temporary in-memory choice visible wherever the service was called.

Later, the Postgres implementation of UserRepository became available. We began removing the extra repository parameter and moving toward an agreed convention: import the repository and use it inside the service. That convention itself would become part of the question.

import { postgresUserRepository } from '../infrastructure/repository/postgres.user.repository';

export async function register(input: RegisterInput): Promise<User> {
  const existing = await postgresUserRepository.findByDocument(input.document);
  // ...
  return postgresUserRepository.create(input);
}

That was the direction of the refactor, not the conclusion of the design. It removed the temporary parameter and restored consistency with the surrounding code, but it also moved the implementation choice directly into the service and removed the visible substitution boundary. In PHP, I knew exactly where each responsibility lived. In TypeScript, the functional style I was working in didn't use those mechanisms.

In the OOP style, each responsibility had an obvious home: a small interface supported Interface Segregation; constructor injection supported Dependency Inversion; a Service Container selected implementations and assembled the graph; tests supplied another implementation of the same interface. The direct-import style looked different enough to raise the real question:

Am I giving up Interface Segregation, Dependency Inversion, centralised composition, or isolated testing? Is functional TypeScript unsuitable for these principles, or am I looking for their OOP mechanisms instead of their functional equivalents? Should this service become a class, or can the functional style preserve the same design qualities from implementation through testing?

The refactor did not reveal a missing principle. It revealed a comparison worth making: for every responsibility that had an obvious home in the OOP style, what technique (and what language structure) gave it an equivalent home in the functional style?

That comparison turned out to be a useful exercise in itself. Not just which pattern to use, but what is a principle, what is a mechanism, and what is a language structure. Where each of those pieces actually plays its role. The rest of this article works through that distinction, one responsibility at a time.

Preserve the responsibilities, change the mechanisms

The comparison becomes clearer when the creation of one service is separated from the composition of the whole application:

Design responsibility Common OOP mechanism Functional TypeScript mechanism
Keep the consumer contract narrow A small, consumer-specific interface A small structural contract
Create one service instance Invoke a class constructor directly or through a dedicated factory Invoke a service factory such as makeRegistrationService(...)
Retain injected dependencies Private object fields The returned functions' lexical closures
Compose the application graph Invoke constructors/factories manually at the composition root, or use a Service Container Invoke service factories manually at the composition root, or use a Service Container
Expose the wired application The entry point receives or resolves a root service/application object The composition function returns an application facade with already-wired operations
Test a consumer in isolation Inject a fake/mock implementation through the constructor Pass a structural or duck-typed double to the service factory

This makes the factory equivalence explicit. makeRegistrationService(users) is doing two familiar jobs at once: its parameters are the functional counterpart of constructor injection, and invoking it is the functional counterpart of constructing the service through new or through a small OOP factory. The returned closure or object of functions is the service instance.

It also keeps composition, composition root, and Service Container distinct:

  • Composition is the activity of assembling the graph.
  • The composition root is the application location where that activity happens.
  • A Service Container is one optional mechanism for performing composition and resolution, often with lifecycle, scope, or discovery features. It can be used in either an OOP or functional application.
  • An explicit composition function is manual composition, not a complete replacement for every container capability.

The composed result is another role again. The application.register operation on the returned application facade is a ready-to-use entry point into the wired graph. It is not a Service Container because callers are not resolving arbitrary services by token, and it is not managing service lifecycles. If application code asks a container for dependencies directly, that moves toward the Service Locator pattern rather than dependency injection.

None of these mechanisms creates a design principle by itself. A large interface still violates ISP. A constructor or factory typed to a concrete class still violates DIP. A container or composition function can centralise poor dependencies as efficiently as good ones. The value comes from assigning each responsibility deliberately.

Import/export syntax is not part of the comparison; both styles use ordinary module boundaries. The meaningful question is where the concrete implementation is selected. In the opening refactor, the Use Case selects postgresUserRepository itself and hard-wires the detail. In the mapped design, the composition root selects it and the service factory receives only the UserRepository contract.

The discovery wasn't any of the individual concepts, they were all familiar. It was the mapping between coding styles. How do OOP and functional TypeScript use different techniques and language structures to preserve Interface Segregation and Dependency Inversion, from implementation through isolated testing? Asking that comparative question put contracts, constructors, factories, closures, composition, containers, facades, and test doubles in their respective roles. The concepts stayed the same; the mechanisms changed.

That comparison opened a second layer: what structure in the language makes each technique possible? Class constructors and object fields create and retain dependencies in the OOP route. First-class functions and lexical closures do the equivalent work in the functional route, while ordinary function application composes the graph. TypeScript's structural type system checks contracts without nominal declarations. JavaScript's dynamic dispatch allows duck-typed doubles at runtime, while TypeScript can check the same doubles structurally before tests run. The organizing chain is therefore not only principle → technique, but design responsibility → technique → enabling language structure.

Map responsibilities, not syntax

Dependency Inversion and dependency injection are related, but they are not the same thing:

  • Dependency Inversion (the D in SOLID) is a principle about direction: high-level policy must not depend on low-level detail; both depend on abstractions.
  • Dependency injection is a technique that serves it: bind a concrete dependency outside the code that consumes it.

The OOP version stores an injected dependency in an object:

class RegistrationService {
  constructor(private readonly users: UserRepository) {}

  async register(input: RegisterInput) {
    const existing = await this.users.findByDocument(input.document);
    // ...
  }
}

The functional version stores the same dependency in a closure:

function makeRegistrationService(users: UserRepository) {
  return {
    async register(input: RegisterInput) {
      const existing = await users.findByDocument(input.document);
      // ...
    },
  };
}

Both consumers depend on UserRepository, not on a database implementation. Both receive the concrete dependency from somewhere else. The difference is where the language stores the provided value: an object field in one case, a lexical environment in the other.

Composition completes the mapping:

// composition/application.ts
import { postgresUserRepository } from '../infrastructure/repository/postgres.user.repository';

function createRegistrationApplication(users: UserRepository) {
  const registration = makeRegistrationService(users);
  return { register: registration.register };
}

const application = createRegistrationApplication(postgresUserRepository);

await application.register(input);

The composition root chooses the Postgres implementation, injects it once, and receives an application facade. Calling application.register does not choose, construct, or locate a repository. In a container-based application, a Service Container may perform this composition at the root. Here the composition function performs it manually. Neither the composition function nor the returned facade is itself a Service Container.

This is also where Interface Segregation fits. UserRepository should describe only what the registration Use Case needs. Structural typing does not guarantee ISP (nothing prevents us from declaring a fat contract), but it removes much of the implementation cost of keeping contracts small. Any value with the required shape can satisfy the consumer's contract without joining an inheritance hierarchy or declaring implements. (More details about implements down in the article.)

Service factories, closures, and composition

Three elements do different work here: the service factory creates a service, its parameters are the injection boundary, and the returned functions' closures retain the injected dependencies.

At the service level, calling makeRegistrationService(users) supplies the contract-typed dependency and creates the service. Every returned function then closes over users. This is the direct functional counterpart of a constructor receiving a value, assigning it to a private field, and producing an object instance, without exposing the dependency to every service call.

At the application level, a composition function can invoke several service factories and build the graph. The next example generalises the earlier registration-only case into a composition function for the full application graph:

interface ApplicationDependencies {
  users: UserRepository;
  mailer: Mailer;
  clock: Clock;
}

function createApplication(dependencies: ApplicationDependencies) {
  const registration = makeRegistrationService(dependencies.users);
  const profile = makeProfileService(dependencies.users);
  const reminders = makeReminderService(dependencies.mailer, dependencies.clock);

  return {
    register: registration.register,
    getProfile: profile.get,
    sendReminders: reminders.send,
  };
}

Call createApplication(...) once at the composition root, and each returned service closes over only the slice of the graph it needs; collectively, those services expose the complete, already-wired application. Production passes database, mail, and system-clock implementations. Tests pass in-memory repositories, recording mailers, and fixed clocks. The consumers remain unchanged.

Closure-based composition can make a Service Container unnecessary for a simple, static graph, but it is not a full replacement for the abstraction. The composition function builds the graph and each returned closure retains its relevant part. Together they cover the container's core composition responsibility: construct the graph centrally, bind implementations to contracts, and expose ready-to-use services. A container may additionally provide autowiring, named bindings, scopes, lifecycle management, decorators, interception, or plugin discovery, and it remains available in functional code when those capabilities earn their cost. In TypeScript, autowiring still needs runtime anchors such as classes or explicit string/symbol tokens; erased structural interfaces cannot be resolved directly. Manual composition is sufficient when those capabilities are not needed.

The two routes can now be summarised without treating module syntax as part of the comparison:

  • OOP: segregated interface → constructor/factory → dependencies retained in object fields → manual or container-assisted composition → root application object
  • Functional TypeScript: structural contract → service factory → dependencies retained in closures → manual or container-assisted composition → application facade

The architecture is preserved because each responsibility still has an explicit home. What changes is the default mechanism: lexical scope replaces instance fields, service factories replace construction through new, structural conformance replaces nominal declarations, and explicit composition can replace container resolution when the graph does not need richer container capabilities.

Anton van Straaten's fictional Qc Na koan captures the first equivalence: "objects are a poor man's closures; closures are a poor man's objects." Both bundle behaviour with retained state. Here that state includes the in-memory repository's Map and, in each service closure, precisely the dependencies that service needs.

Structural typing: the conformance mechanism

Service factories and closures answer how one service is created and retains its injected dependencies; the composition root answers where the full graph is assembled. Structural typing answers a simpler question: how does the compiler know a value satisfies the contract its consumer expects? It's what makes the functional mapping safe, not just short.

In a structurally typed system, a value satisfies a type by having the right shape: the required members, with compatible types. Names and declarations are irrelevant. UserRepository defines one such shape:

interface UserRepository {
  findByDocument(document: string): Promise<User | null>;
  create(data: NewUser): Promise<User>;
}

Any value with those two methods, and compatible signatures, has that shape. So when the in-memory repository assigns its object literal to that type. Consider:

export const inMemoryUserRepository: UserRepository = { findByDocument, create };

That single line is the conformance check. It's compile-time, exhaustive, and free: if the object is missing a method, has one with an incompatible signature, or the contract later grows, this line stops compiling. No implements, no registration, no adapter class. And it generalises: any object literal, class instance, or factory result that has the shape is a UserRepository. The contract is decoupled from every implementation's ancestry, which is why an in-memory object literal and a Postgres-backed implementation can be substituted at the composition root without changing a consumer.

You can watch the check evaporate the moment it's done its job. The in-memory repository didn't need implements. A Postgres-backed class can still use it for clarity, but it's optional, and it disappears at runtime. The TypeScript source:

class PostgresUserRepository implements UserRepository {
  async findByDocument(document: string) { /* ... */ }
  async create(data: NewUser) { /* ... */ }
}

compiles to plain JavaScript with no trace of the contract:

class PostgresUserRepository {
  async findByDocument(document) { /* ... */ }
  async create(data) { /* ... */ }
}

The interface and implements annotation disappear because they are type-system constructs. The class remains because a class is also a runtime JavaScript construct. Conformance was proven once, before the program ran, and then discarded. It's also why a TypeScript DI container can't autowire by interface the way C# or PHP can: at runtime there's no UserRepository to resolve against, only classes and string/symbol tokens. The same erasure that keeps contracts lightweight is the reason reflection-based containers cannot autowire by interface and must use runtime classes or explicit tokens.

This is also where the I of SOLID becomes cheaper to practise. A small contract requires little implementation ceremony (no base class, no implements, no registration), so keeping interfaces role-specific adds less friction. In a nominal OOP language, each new segregated interface adds a declaration that its implementations must adopt; structural typing removes that declaration tax. It does not guarantee Interface Segregation, but it makes the principle easier to apply consistently.

The contrast that makes this concrete is a nominal language, PHP, the reference OOP case here:

final class PostgresUserRepository implements UserRepository { /* ... */ }

function register(UserRepository $users): void { /* ... */ }

Here conformance is declared. An object with exactly the right methods but no implements UserRepository is not a UserRepository: pass it to register() and PHP throws a TypeError. Same shape, wrong ancestry, rejected. This is one reason adapters and containers become more prominent in nominal ecosystems: conformance is explicitly declared, so wiring becomes a first-class activity with its own tooling.

One distinction worth stating now, because it drives the rest of the post: TypeScript is structural for interfaces, object types, and (mostly) classes, and it checks that conformance statically, before the program runs. "Checked statically" is easy to wave past, but it's the entire difference between structural typing and the thing it gets mistaken for. And the fastest place to feel that difference isn't in production at all. It's the moment you sit down to write a test.

And I am a Test-Driven Developer. Therefore...

Testing: where duck typing walks back in

The style survives contact with production easily enough. The seam is typed, the closure captures a contract-typed value. Where it gets interesting is the first time you write a test.

To test a service in isolation, you need a stand-in for its dependency, a repository that never touches a database. In JS/TS we've done this for years with an ad-hoc object: just the methods the test exercises, nothing more. And here the old instinct kicks in, reach for a loose double:

// duck-typed double: no contract in sight
const users = { findByDocument: async () => null } as any;

This works. The test runs, the service calls findByDocument, the call resolves. That's duck typing: the object is accepted because, at the moment of the call, it happens to have the method. Nothing checked its shape ahead of time; the as any explicitly threw the check away.

And it raises the question this whole post circles: is that the same thing as structural typing? It feels identical, both say "shape is enough, ancestry is irrelevant." But the difference is exactly the when. Type the same double against the contract instead:

// structurally-typed double: checked against the contract, now
const users: UserRepository = {
  findByDocument: async () => null,
  create: async (data) => ({ id: 'x', ...data, createdAt: new Date() }),
};

Now the compiler enforces the whole shape before the test runs. Forget create, or let the real UserRepository grow a method, and this double stops compiling; the test tells you it drifted. The as any version compiles happily and only fails later, at runtime, if that path is even exercised.

Same philosophy, opposite failure mode. The loose double (as any) is widely used and genuinely convenient, and it's quietly the reason so many of us assume duck typing and structural typing are one thing. They aren't: typing the double : UserRepository is the small discipline that keeps a test double in the structural camp, and it's worth doing. But the recommendation isn't the point here. The point is that the moment you write a mock is the moment the two concepts visibly separate, which is exactly what the next section pulls apart.

Untangling the five concepts

Five ideas have been doing distinct work so far. Placing them side by side makes the boundaries explicit:

Concept What it is What it is not
Dependency inversion / injection DIP: a principle about direction; depend on abstractions (the D in SOLID). DI: a technique serving it; bind concretions elsewhere A framework, a container, or anything inherently class-shaped
OOP constructs Classes, constructors, implements; one mechanism for DI and encapsulation The definition of DI, or a requirement for Interface Segregation or Dependency Inversion
Closures Functions capturing their environment; another mechanism for retained private state, injected dependencies, and explicit graph composition A Service Container abstraction by definition, or a hack until "real" OOP arrives
Structural typing Static conformance decided by shape; checked before the program runs A dynamic-language feature; permission to skip contracts
Duck typing Runtime conformance: "if it quacks"; discovered at the call site, or not at all A synonym for structural typing

Read down the "what it is not" column and the responsibilities separate cleanly. Dependency injection is not a framework: an explicit composition function can bind the graph as truly as a container. OOP constructs are not the definition of DI: they're one mechanism among several, the one that happens to dominate nominal ecosystems. Closures are not a hack or a container by definition: they are a language mechanism that can retain one dependency or the complete composed graph.

The two that cause the most trouble are the last ones: structural typing vs duck typing. They share a philosophy, behaviour over ancestry, shape over name, and in TypeScript they even look identical to write, which is why they get treated as one thing. They are not. The difference is when the shape is checked. Structural typing checks it at compile time, by a type checker, before the program runs; the failure is a red squiggle in your editor. Duck typing checks it at runtime, at the moment of the call, by the call itself; the failure is a thrown error, or worse, silence, if the missing method simply never gets reached on that path.

Which answers the question the testing section left open: if structural typing isn't what makes duck typing possible, what does? The enabler is dynamic dispatch, the language resolving x.quack() against whatever x actually is at call time, with no prior type commitment required. That's a property of the runtime, not of the type system, and it's independent of whether conformance is judged by shape or by declaration. The clean proof is Python: it has had duck typing forever, with no structural type system at all; typing.Protocol (PEP 544) later added static structural checking as an opt-in for external type checkers, and the duck-typed runtime underneath didn't change one bit. So duck typing and structural typing aren't two names for one idea, they're independent answers to two different questions, how? and when?, and a language can choose each answer separately.

The language field guide

Put the two questions on two axes, how conformance is decided (by shape, or by declaration) and when it's checked (statically at compile time, or at runtime), and every language lands in a cell:

Checked statically (compile time) Checked at runtime
By shape TypeScript, Go (structural typing) Python, JS, Ruby (duck typing)
By declaration C#, Java (nominal + static) PHP with type hints (nominal + runtime)

The cells people forget are the interesting ones.

  • TypeScript: shape, static, and loose. An ad-hoc object literal conforms on the spot, no named type required. The cheapest contracts of any mainstream language. TypeScript provides static structural checking over a dynamically typed JavaScript runtime, so once the types are erased, what's left is duck typing.
  • Go: shape, static, and not loose. Interfaces are satisfied implicitly (structural, a type never says implements), but conformance still needs methods declared on a named type; there are no ad-hoc conforming literals and no runtime "try the call and see." Interface satisfaction operates through named interface types rather than TypeScript-style ad-hoc object compatibility. This is the answer to "is there structural typing without any duck typing?". Go is it, and it isn't a compromise. The idiomatic Go move of defining tiny consumer-side interfaces is Interface Segregation and structural typing working together, straight out of the textbook.
  • Python / JS / Ruby: shape, runtime. Classic duck typing, enabled by dynamic dispatch. Python then bolts on optional static structural typing via typing.Protocol (PEP 544) for anyone running a type checker, without touching the duck-typed runtime, the cleanest demonstration that the two are separable.
  • PHP: declaration, runtime. Nominal, but enforced at call time. With type declarations, objects of classes that declare implements pass; a mismatch is a TypeError when the call happens, not a compile error (PHPStan/Psalm drag the check earlier, as a lint). Drop the declarations and PHP becomes dynamically typed: no type enforcement on parameters, method calls resolved at runtime, but objects are still class instances, not ad-hoc shapes. That double identity, nominal-when-typed, dynamic-when-not, plus autowiring by type declaration is the same story that made the containers from section 3 inevitable.
  • C# / Java: declaration, static. Nominal, checked at compile time. The full ceremony (declare the interface, implements it, register it, inject it) and perfectly capable of clean dependency-inverted designs; the container exists to amortise the wiring cost the nominal type system creates in the first place.

The grid shows these are independent axes, and real languages occupy all four cells. Go is structural and statically checked, but without TypeScript's ad-hoc object compatibility; TypeScript is structural-and-loose yet fully static; Python is duck-typed with an optional structural upgrade; PHP is nominal without being static, the quadrant people forget exists. "Structural" and "duck" are not synonyms, and "nominal" and "static" are not the same axis. Once the grid is visible, the testing question becomes precise: a TypeScript double can rely on JavaScript's runtime duck typing, or it can be checked structurally before the test runs. The object may look the same; the guarantee is different.

A good way to use them

None of this argues against classes, and none of it argues against containers. It argues for choosing the mechanism on purpose. A few defaults fall out of everything above:

  • Keep contracts in the domain, named in the domain's language. UserRepository belongs in domain/user.ts, not in the infrastructure that implements it; the consumer owns the interface, the implementation conforms to it. This is the Go consumer-defined-interface instinct and Interface Segregation in one move.
  • For simple functional services, a factory + closure is a natural default. The dependency is captured once and stays private. Keep parameter passing for genuinely local composition or deliberate scaffolding. Put the complete graph in an explicit composition root (createApplication or equivalent), invoke it once with production implementations, and retain the returned application facade as the wired root object. That is where implementation choices remain centralised and tests gain a single, typed substitution boundary.
  • Treat class-vs-closure as a mechanism choice, not a moral one. Pick by team convention and by whether you genuinely need identity or lifecycle semantics, not because one of them is "real" dependency injection. They both are.

It's worth naming the design principle underneath, because it's the reason the cheap option is also the good one. In Vlad Khononov's Balanced Coupling model, a shared contract is the weakest, most distance-tolerant form of coupling between two components, exactly what you want across a module boundary, where the two sides may change and ship independently. Structural typing makes that weakest level of coupling nearly free to express, so in TypeScript the best-balanced design and the least-ceremony design are the same design. The one discipline the model insists on: keep the wiring explicit and greppable, a named seam you can find, because Balanced Coupling's warning is against implicit coupling, and implicit coupling is also structural typing's failure mode. Which is the honest counterpoint.

The honest counterpoint

The price of making conformance free is that conformance becomes implicit. Anything with the right shape satisfies a contract, including things that match only by accident. A single-method contract like { execute(): void } is satisfied by half the objects in a codebase; the type checker will happily accept a wrong one that merely fits. The narrower and more generic the contract, the more coincidental matches it invites, so the defence is to keep contracts role-named and non-trivial (UserRepository, not Doer), so that matching the shape actually means matching the intent.

Implicitness costs tooling, too. Without implements, "find all implementations of this interface" is a weaker query than in a nominal language, and renaming a contract member doesn't announce, at the declaration site, everyone who just broke. You lean harder on the compiler and on find-references, and a little less on the class hierarchy telling you who's involved.

The mitigation turns out to be the same thing as the recommendation from the previous section: an explicit, greppable composition root. A deliberate conformance point, const impl: Contract = ..., typed against the contract, living in that known location, turns implicit shape-matching back into a visible, findable decision. It's also, not coincidentally, the same discipline that keeps a test double structural rather than duck-typed. The functional style doesn't remove the need to be deliberate about coupling; it relocates the deliberateness from the type declaration to the wiring, and rewards you for keeping that wiring in the open.

Checklist

A quick test for "am I doing DI properly in this style?", whether or not you reached for a class:

  • Does the consumer depend on a contract, not a concrete implementation? (Take the contract as a factory parameter and capture it.)
  • Is the binding external to the consumer, captured by a service factory and selected at the composition root, rather than constructed inline?
  • Is every implementation, including test doubles, typed against the contract (: UserRepository), so drift fails at compile time instead of at runtime?
  • Are contracts small and role-named, so structural matching signals intent rather than coincidence?
  • Is the complete dependency graph explicit and greppable in one composition root, rather than having consumers select concrete implementations throughout the application?
  • Did you pick class-vs-closure for a real reason (convention, lifecycle, identity), not out of habit or a belief that one is "real" DI?

Recap

The article started with a refactor that removed a temporary parameter and raised a question: was I giving up Interface Segregation and Dependency Inversion by moving from OOP to functional TypeScript, or was I looking for OOP mechanisms where functional equivalents existed?

The answer turned out to be a comparison worth making. For every responsibility that had an obvious home in OOP, there was a functional technique that gave it an equivalent home:

  • Interface Segregation lives in small, role-named contracts. Structural typing removes the declaration tax that keeps contracts fat in nominal ecosystems.
  • Dependency Inversion lives in depending on abstractions, not on concretions. A factory parameter captures a contract-typed dependency as cleanly as a constructor.
  • Dependency Injection lives in binding concretions outside the consumer. A service factory and closure retain injected state as truly as an object field.
  • Composition lives in a single root where implementations are selected and the graph is assembled. A composition function does this as truly as a Service Container.
  • Conformance lives in shape, not ancestry. Structural typing checks it statically, before the program runs. Duck typing discovers it at runtime, at the call site. Same philosophy, different when.
  • The language field guide put that distinction on two independent axes:
    • How conformance is decided: by shape or by declaration.
    • When it's checked: statically at compile time, or at runtime.
  • Real languages occupy all four cells:
    • Go is structural and statically checked, but without ad-hoc object compatibility.
    • TypeScript is structural and loose, yet fully static.
    • Python is duck-typed with an optional structural upgrade.
    • PHP is nominal without being static.
  • "Structural" and "duck" are not synonyms, and "nominal" and "static" are not the same axis.
  • The honest counterpoint: making conformance free makes it implicit. Anything with the right shape satisfies a contract, including things that match only by accident. The defence is role-named, non-trivial contracts and an explicit, greppable composition root. A deliberate conformance point, typed against the contract, turns implicit shape-matching back into a visible, findable decision.
  • The principles stayed the same. The mechanisms changed:
    • Classes, constructors, and implements are one set of mechanisms.
    • Closures, factories, and structural contracts are another.
  • Neither set is the definition of the principles they carry. Picking between them is a mechanism choice, not a moral one.
  • The one discipline that travels across both styles: keep the wiring explicit and greppable. A named seam you can find is the difference between deliberate coupling and accidental coupling, whether the seam is a constructor parameter or a factory argument.

Glossary

  • OOP — Object-Oriented Programming: a programming style that organises state and behaviour around objects. For comparison, this article uses a common class-based arrangement: interfaces, constructors, implementations, and a Service Container.
  • SOLID: a family of five design principles associated with maintainable and adaptable software. This article focuses only on its I and D.
  • ISP — Interface Segregation Principle: consumers should not be forced to depend on operations they do not use; prefer small, role-specific contracts.
  • DIP — Dependency Inversion Principle: high-level policy should not depend directly on low-level details; both should depend on abstractions, and details should depend on those abstractions.
  • DI — Dependency Injection: a technique for supplying dependencies from outside the consumer. DI can support DIP, but the two are not synonyms.
  • Closure: a function together with the lexical environment it retains, allowing injected values to remain available after the factory that received them has returned.
  • Service Container: a central assembler or registry that selects implementations and wires services, often with additional lifecycle or resolution features.
  • Composition root: the single application location where concrete implementations are selected and the dependency graph is assembled.
  • Seam: a place in code where behaviour can be varied without editing the code that uses it. Coined by Michael Feathers in Working Effectively with Legacy Code. In this article, the repository parameter is a seam: you swap implementations at the call site without touching the service.

Further reading and references

Glossary concepts

Language and design mechanics

Categorias
Tropeçando

Tropeçando 120

AI Management & Organizational Restructuring

The Foreman Problem: Managing Teams When Your Best Worker Isn't Human - Willian Correa

Every major technology shift invented a new management role. Steam power → foreman. Office computing → project manager. Internet → product manager. AI is doing the same, but this time the failure mode is invisible: confident, polished, wrong output. The new job is not directing effort but verifying that things that look like they're running actually are.

Who Will Be the Senior Engineers of 2035? - James Stanier

The traditional junior-to-senior pipeline is breaking: entry-level tech postings down 67% since 2022, junior employment down ~20%. Firms adopting AI saw junior employment fall 7.7% vs non-adopters. 54% of engineering leaders plan to hire fewer juniors.

Compound Engineering & Code Health

The Compounding Software Factory - Luca Rossi (Software Factory series, Part 3 of 3)

What causes teams to degrade: poor coding hygiene (bad testing, poor code health, missing abstractions), failure to capture knowledge (no ADRs, no playbooks, no snapshots), and building the wrong things.

AI Coding Meets Code Health - Stuart Caborn

Loveholidays' journey to becoming an AI-first engineering organization. Core thesis: code health is the foundation for successful AI adoption. By deliberately investing in code health metrics before adopting AI, they achieved 80+ deployments/month, 60% AI-written code, <1% change failure rate, all while maintaining elite code health.

Security & Infrastructure

The (In)security Landscape of AI-Powered GitHub Actions - Shay Berkovich

AI-powered GitHub Actions from vendors like OpenAI, Anthropic, and Google are now running in thousands of public workflows. Research found bypasses of non-default configurations letting any external attacker trigger AI execution, a novel secret exfiltration vector for dynamically-created credential files, and widespread misconfigurations in production workflows.

The Invisible Engineering Behind Lambda's Network - Werner Vogels

A decade-long story of invisible infrastructure engineering by Lambda's networking team.

Career & Token Economics

Tokenmaxxing Is the Budget Game Played With AI Tokens - Willian Correa

Tokenmaxxing — maximising AI token consumption for visibility — is the corporate "use it or lose it" budget game in a new currency. Meta's internal "Claudeonomics" leaderboard ranked 85K employees by token consumption; top user burned 281B tokens in 30 days.

Tools

Use Compose Watch

Docker bind volumes gets a supercharge. Compose Watch does not replace bind mounts but exists as a companion specifically suited to developing in containers.

More importantly, watch allows for greater granularity than is practical with a bind mount. Watch rules let you ignore specific files or entire directories within the watched tree.
For example, in a Node.js project, it's not recommended to sync the node_modules/ directory. Even though JavaScript is interpreted, npm packages can contain native code that is not portable across platforms.

Categorias
Tropeçando

Tropeçando 119

How to Grow your Software Factory

Luca Rossi argues that the right measure of AI effectiveness isn't lines of code but leverage — how much output you get per unit of human input. Teams progress through three stages: writing full specs for everything, then encoding knowledge into shared rules (like AGENTS.md), and finally building reusable modules that enforce correctness by design.

The security case for serverless just got stronger

AI agents can now scan an entire open-source codebase for exploitable vulnerabilities in hours.

Frontier models carry the complete library of known bug classes in their weights. So you can simply point an AI agent at a codebase and tell it to find zero-days.

This isn't theoretical.

Yan Cui highlights that AI agents can now find real zero-days in open-source codebases at scale, shrinking the patch window from weeks to hours. Serverless and managed services have a structural advantage because AWS patches the runtime for you. The practical takeaways: eliminate long-lived AWS keys everywhere, treat LLM API keys like credentials, and scan your repos for exposed secrets.

Do not use secrets in environment variables and here's how to do it better

Every Layer of Review Makes You 10x Slower

Each approval layer adds 10x wall clock time, and AI can't fix that. It only speeds up the first step. Drawing on Deming and the Toyota Production System, the argument is that review layers hide root causes rather than fixing them. The memorable line: "The job of a code reviewer isn't to review code — it's to figure out how to obsolete their review comment, that whole class of comment, forever."

The common thread across all four: the bottleneck isn't writing code, it's the systems around it. Whether it's review layers, security patching, or AI leverage, the answer is the same: engineer quality into the system itself through tests, automation, modules, and clear interfaces, rather than adding layers of inspection after the fact.

Claude Code cache chaos creates quota complaints

Anthropic changed the prompt cache TTL from 1 hour to 5 minutes in March. Long, high-context sessions hit quota limits much faster. Pro users report as few as 2 prompts per 5 hours. Leaving your machine for >1 hour = full cache miss on the 1M token context. They're considering reducing the default to 400K tokens.

Token consumption matters more than ever. The next two tools address this from both ends.

Caveman — Output Token Compression

Constrains LLM output to minimal-token structures. Strips pleasantries and padding, keeps code and technical content. Up to 87% output token reduction. Paper shows brevity constraints improve accuracy by 26pp.

RTK (Rust Token Killer) — Input Token Compression

Intercepts shell command outputs (git, ls, grep, test runners, docker, AWS CLI — 100+ commands) and compresses them before they reach the LLM context. 60-90% input token reduction, < 10ms overhead.

Works with: Claude Code, Copilot, Gemini CLI, Codex, Cursor, Windsurf, Cline.

Categorias
Tropeçando

Tropeçando 118

Your AI Coding agent doesn’t know when to ask for help

Why do multi-agent coding systems fall apart on complex, real-world tasks?

How to Manage Context in AI Coding

Focus on building multiplayer, dynamic systems that provide the right information reliably, rather than crafting magical wording. Design workflows where AI can fetch what it needs automatically.

Value Object

When programming, I often find it's useful to represent things as a compound.

Range - Further Enterprise Application Architecture development

It's quite common to see comparisons where a value is checked against a range of values. Ranges are usually handled by a pair of values and you check against them both. Range instead uses a single object to represent the range as a whole, and then provides the relevant operations to test to see if values fall in the range and to compare ranges.

JDK 17 Memory Bloat in Containers: A Post-Mortem

I just love runtime upgrades. Runtime upgrade are very important. And they need careful planning. Not unusual that they teach us important lessons for the next upgrade.

When engineering teams modernize Java applications, the shift from JDK 8 to newer Long-Term Support (LTS) versions, such as JDK 11, 17, and soon 21, might seem straightforward at first. Since Java maintains backward compatibility, it's easy to assume that the runtime behavior will remain largely unchanged. However, that's far from reality.

My Fitbit Buzzed and I Understood Enshittification

My Fitbit started buzzing at me a year ago. “It looks like you’re exercising.”

Product development is also an exercise in human relationships. And when we reduce those relationships to metrics, we lose something essential. We lose the ability to say, “This would be rude.” We lose the ability to treat users like people instead of engagement vectors.

Using the Middleware Pattern to Extend PHP Libraries

PSR-15 did not invent middleware. But it showed the PHP community what a well-designed, typed middleware interface looks like. There is no reason to leave that idea at the HTTP layer.

If you maintain a PHP library with any non-trivial processing, consider building middleware support in from day one. Your users will thank you, and so will your future self.

Categorias
Tropeçando

Tropeçando 117

How far can we push AI autonomy in code generation?

We ran a series of experiments to explore how far Generative AI can currently be pushed toward autonomously developing high-quality, up-to-date software without human intervention. As a test case, we created an agentic workflow to build a simple Spring Boot application end to end. We found that the workflow could ultimately generate these simple applications, but still observed significant issues in the results—especially as we increased the complexity. The model would generate features we hadn't asked for, make shifting assumptions around gaps in the requirements, and declare success even when tests were failing. We concluded that while many of our strategies — such as reusable prompts or a reference application — are valuable for enhancing AI-assisted workflows, a human in the loop to supervise generation remains essential.

Announcing the Official PHP SDK for MCP

The PHP Foundation, Anthropic’s MCP team, and Symfony are collaborating on the official PHP SDK for the Model Context Protocol (MCP). Our goal is a framework-agnostic, production-ready reference implementation the PHP ecosystem can rely on.

Covariance and Contravariance in PHP

Before we dive into the details and code examples, let me quickly define covariance and contravariance:

Covariance: Making something more specific
Contravariance: Making something less specific

Now let's dive in and see how these concepts apply to PHP.

Break Stuff on Purpose

Strengthen your system’s ability to recover by intentionally causing and resolving failures

Nothing Beats Kindness

Categorias
Programming

Principles in Refactoring – Slowing Down New Features?

The whole purpose of refactoring is to make us program faster, producing more value with less effort.

and

But I think the most dangerous way that people get trapped is when they try to justify refactoring in terms of "clean code", "good engineering practice", or similar moral reasons. The point of refactoring isn't to show how sparkly a code base is -- it is purely economic. We refactor because it makes us faster -- fastor add features, faster to fix bugs.

-- From Refactoring: Improving the Design of Existing Code (Martin Fowler and Kent Beck), page 56

Categorias
Programming

The Rule of Three

The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor.

-- Don Roberts

Categorias
Tropeçando

Tropeçando 113

Neon

Serverless PostgreSQL database with real zero-scaling. The fully managed serverless Postgres with a generous free tier. We separate storage and compute to offer autoscaling, branching, and bottomless storage.

Compute scales dynamically to ensure you're ready for peak hours. Compute scales to zero and cold storage offloads to S3 for cost efficiency. Create a fully managed serverless Postgres instance in seconds.

Make your app faster with PHP 8.3

PHP 8.3 is the latest version of PHP. It has exciting new features and major improvements in performance. By upgrading to 8.3, you can achieve a significant increase in speed. In this article, we dive into how PHP 8.3 can be a game changer. It can speed up your application's performance.

OWASP Top 10 Explained: SQL Injection

SQL Injection (SQLi) is a code injection technique that exploits a security vulnerability occurring in the database layer of an application.

The vulnerability is present when user inputs are either improperly filtered for string literal escape characters embedded in SQL statements or user input is not strongly typed and thereby unexpectedly executed.

This allows an attacker to manipulate SQL queries, enabling them to unauthorized access, modify, and delete data in the database. This can lead to significant breaches of confidentiality, integrity, and availability, ranging from unauthorized viewing of data to complete database compromise.

15 Quick Useful Tips for AWS CDK Engineers

In this short article, we will cover 15 useful tips with accompanying code snippets for AWS CDK users.

Implementing DTOs, Mappers & the Repository Pattern using the Sequelize ORM [with Examples] - DDD w/ TypeScript

There are several patterns that we can utilize in order to handle data access concerns in Domain-Driven Design. In this article, we talk about the role of DTOs, repositories & data mappers in DDD.

Categorias
Technology

Notes – ServerlessDays NZ 2024

Those are my notes for ServelessDays NZ - Auckland, at 24th May 2024.

Sheen Brisals - Think, Architect, and Build Serverless Applications as Set Pieces

During ServerlessDaysNZ Sheen Brisals gave the talk Think, Architect, Build, Sustain Serverless Application Set Pieces. It was full of important insights to Set Pieces and sustain Serverless Applications.

I particularly liked how he touched on the fact that legacy applications being rewritten to Serverless is a thing, as this is everywhere being part of lots of engineers' lives.

More than that, Brisals highlighted how patterns and pivotal for a maintainable and reliable application, despite the execution model:

  • Identify Domains so you can decouple a domain to rewrite it more effectively
  • Complexity is better abstracted, becoming simpler, when you know and apply good proven Patterns -- the exception is to invent a new one
  • Design Patterns, Architecture Patterns, Execution Model patterns, Software Design, etc, will improve the quality of your Application. As Serverless will likely push you to learn them, you have the opportunity to develop as an Architect
  • The Serverless should help you to think in the whole picture, as the settled pieces need communication between them, therefore optimising value to the end-user

Unfortunately, I was not selected to win the book Serverless Development on AWS, but for those who won, I wish they could learn a lot there. What a great indication of how good a fellow is Sheen. Giving away those books is a gigantic contribution to the community!

I am very pleased to know you in person, Sheen.

This presentation talked a lot with Michael Walmsley's. So nice.

Heitor Lessa - Let Them Retry: Idempotency for the Rest of Us

Despite being common to talk or to assess if a given application or infrastructure follows best practices and great architectural patterns, implementing this is a challenge for development teams for different reasons.

Heitor Lessa, in his talk "Let Them Retry: Idempotency for the Rest of Us", demonstrates how a tool that improves the Developer Experience bringing the implementation of the patterns close to the code is powerful to win adoption. PowerTools is a developer toolkit to accelerate development providing interfaces and abstractions to implement Serverless best practices.

Heitor used a sample code, emulating an existent codebase, from an application already working in Production. We had the opportunity to see the appeal of PowerTools. Usually, Idempotency (to handle duplicated transactions) is associated with a good amount of change in the code. Still, PowerTools was designed to introduce no or very few impacts to a code that is very dangerous to change. As building blocks, adding more complex functionalities, such as caching, payload tempering and failure mode.

The existence of tools like PowerTools reinforces how implementing good and proven software (and architectural) patterns is pivotal for a scalable and reliable application. The Serverless execution mode can mislead to relaxed code, but that would weaken the performance and stability of an application. The lesson is that working smarter is applying known solutions for specific problems.

PowerTools provides a wide range of functionalities, not surprisingly being able to match Well-Architected frameworks in their implementation: Secrets/System Manager Parameters, Event Source Data Classes, Validation, Feature Flag, Idempotency, Data Masking, Streaming, Middleware, JMESPath, Batch processing, Metrics, Tracing. We avoid writing boilerplates, repeated code and even the need to create a shared lib of constructs ourselves. The community is improving it.

PowerTools is a helpful tool to implement these features. This is an opportunity to learn and deep dive into best practices and designs. It also enhances how you observe and monitor your application. It is a serious tool to consider if you intend to leverage how your code is executed, deployed, monitored and performed.

In his talk, Heitor implemented, live in the meeting, Idempotency into a legacy code. He enriched it with failure modes, caching, payload tampering and order tolerance. So, PowerTools is also very easy and quick to use.

Best practices for everyone

  • Heitor Lessa

Michael Walmsley - Unleashing Serverless Scalability on AWS: Practical Strategies and Proven Patterns

Some started Michael Walmsley introduction saying "A fantastic human being...". And I will start from there as well because I have experienced that myself.

I bumped into Michael while walking to the conference venue. I first heard about it from a great friend, Joshua Katz, who was impressed with Michael. It was a very pleasant walk while sharing quick impressions of being AWS Community Builders and excitement about the conference.

It happens that Michael is now an AWS Hero with many years of experience to share. One of the first things he said in his talk was replaying Suzana Melo Moraes (you should listen to this girl - so inspiring), who has three years in tech, when she was saying that, mostly every day, she struggles with something usually starting from having no idea how to fix a particular problem she was assigned to solve. Michael sympathised, saying that, even after 30 years, there are days that things happen to him the same way. This happens in everyone involved in this field and it was so humbling coming from him.

As usual, Michael doesn't keep secrets by himself but shares insightful tips. His presentation was about Unleashing Serverless Scalability on AWS:

  • Start the design with the needed scalability in mind (can you see that links to Sheen Brassals talk?)
  • Master and understand well the limits, they are there for a reason and as early you design your application to work with them, better design your application and scalable-ready it is
  • Events, Messages, and Commands are the way of communication for Serverless and a must-know subject
  • Do not ignore Flow Control
  • Break your application limits before someone else does -- use performance tests in your favour
  • Study and use proven patterns (check https://serverlessland.com)

Brad Jacques - Delivering at pace while evolving a Serverless architecture

Brad Jacques delivered a talk titled "Delivering at pace while evolving a Serverless architecture" at ServerlessDays NZ. Brad covered a challenging project where file manipulation use cases were an important feature.

"Complexity is everywhere". Brad could not help it advise that a successful delivery starts from breaking the complexity into pieces, to plan ahead of time and to do the simple things first. He mentioned that the deadline was short, affirming it was the right strategy to evolve the architecture.

He also stressed the use of established patterns for success, such as breaking down complexity, identifying domains and context boundaries, and understanding limits and messaging.

It was also important how the work was planned with the team. Having a small committed team, fast feedback loops and continuous measurement were key to proving the solution was correct.

The summary is so great that I will copy it here entirely:

  • Do the simple thing first
  • Small teams with a fast feedback loop (showcase often)
  • Identify risk early, shift left, and spike
  • Continuously measure performance, and stress test
  • Isolate context boundaries
  • The solution must prove itself correct

Brad's insights were based on his experience with a new project for a major client at a consultancy company. However, it was clear that the principles and strategies he shared apply to any application, in any industry, and of any size.

His parting advice was to "evolve your architecture, measure, and make decisions throughout the process."

Categorias
Tropeçando

Tropeçando 111

Don't do this: creating useless indexes

This is why, when I’m called for a performance problem (or for an audit), my first take is to look at the size of the data compared to the size of the indexes. If you store more indexes than data for a transactional workload, that’s bad. The worst I’ve seen was a database with 12 times more indexes stored on disk than data! Of course, it was a transactional workload… Would you buy a cooking book with 10 pages of recipes and 120 pages of indexes at the end of the book?

The problem with indexes is that each time you write (insert, update, delete), you will have to write to the indexes too! That can become very costly in resources and time.

Functional Classes

A place for everything, and everything in its place.

What is a class? According to the dictionary a class is:

A set, collection, group, or configuration containing members regarded as having certain attributes or traits in common; a kind or category.

The Simple Class

I work in many legacy code bases, and in fact, I’ve made it a big part of my career. I love diving into big monoliths that have grown out of proportion and tidying them up. One of the best parts of that work is rewriting a God class into a collection of small reusable classes. Let’s take a look at what makes a simple class great.

The economics of clean code

Code smarter. Code balanced. That is OK to have some debt. But pay them off quickly.