Skip to content

no-portless-adapter ​

A driven adapter exists to implement a port: every class in driven/<technology>/adapters/ extends one, and every port is declared by the domain.

Rule
layers/no-portless-adapter
Category
Layers: what each layer may depend on
Reports
A driven adapter that extends no port, a port declared outside the domain
Applies to
Every class in driven/**/adapters/, and every abstract class that extends Port
Turn off
"layers/no-portless-adapter": "off"

Why ​

A Mailer class in driven/smtp/adapters/ sends emails, and the handler calls it directly. The application now depends on SMTP: testing a use case sends mail, and moving to an email API means rewriting the handler.

The fix

The domain says what it needs in its own words, as a port; the adapter extends that port. The handler only knows the port, so the adapter can be swapped in a test or when the technology changes.

What it checks ​

1Every adapter extends a portEvery class in a driven/**/adapters/ folder extends a Port, directly or through a repository, Outbox, EventPublisher, UnitOfWork, Clock or IdGenerator.
2Every port lives in the domainEvery abstract class that extends Port is declared in domain/ports/ or domain/repositories/.

Abstract base classes for your adapters may also live in driven/**/adapters/, as long as they extend a port.

What it reports ​

src/ordering/driven/smtp/adapters/mailer.adapter.ts:1
  layers/no-portless-adapter: Mailer is a driven adapter but
  extends no Port: extend the port it implements.

src/ordering/application/commands/notifications.ts:3
  layers/no-portless-adapter: The port Notifications is declared
  outside the domain: move it to domain/ports/ or
  domain/repositories/.

Fix it ​

Extend the port the adapter implements ​

So that the application depends on what it needs and not on the technology, declare a port in the domain and make the adapter extend it. Name the adapter after its technology and its port.

❌ Avoid: src/ordering/driven/smtp/adapters/mailer.adapter.ts
ts
export class Mailer {
	async send(to: string, body: string): Promise<void> {}
}
✅ Prefer: src/ordering/driven/smtp/adapters/smtp-notifications.adapter.ts
ts
import { Notifications } from "../../../domain/ports/notifications.port";

export class SmtpNotifications extends Notifications {
	async orderPlaced(email: string): Promise<void> {}
}

Declare ports in the domain ​

So that the domain knows every dependency it relies on, a port declared in the application or next to an adapter moves to domain/ports/, or to domain/repositories/ for a repository.

❌ Avoid: src/ordering/application/commands/notifications.ts
ts
export abstract class Notifications extends Port {
	abstract orderPlaced(email: string): Promise<void>;
}
✅ Prefer: src/ordering/domain/ports/notifications.port.ts
ts
export abstract class Notifications extends Port {
	abstract orderPlaced(email: string): Promise<void>;
}

Turn it off ​

alveolus.config.ts
ts
rules: { "layers/no-portless-adapter": "off" },

On an existing project, prefer a baseline: new adapters extend a port while you wrap the old ones.

See also ​

Released under the MIT License.