50%

الدرس 1 من 2

Rings, and the Rule That Points Inward

Clean and onion architecture are the same structure under two names: business rules at the centre, everything replaceable at the edge, and source dependencies that only ever point inward.

قراءة 11 دقيقة

Jeffrey Palermo described onion architecture in 2008; Robert C. Martin published clean architecture in 2012. Alistair Cockburn had described hexagonal architecture — the subject of the next module — in 2005. All three are variations on one idea, and it is worth saying plainly at the start: if you understand the dependency rule, you understand all of them, and the differences between the names are much smaller than the literature suggests.

The rings, from centre outward

Each ring may depend on rings inside it and never on rings outside it. The direction of control at runtime is the opposite.

Four concentric rings. At the centre, entities holding enterprise-wide business rules. Around them, use cases holding application-specific rules. Around those, interface adapters: controllers, presenters, repository implementations. At the outside, frameworks and drivers: the web framework, the database, external services.

Entities (centre)

Business rules

valid regardless of application

Use cases

Application rules

orchestrates entities

Ports

interfaces the use case needs

Interface adapters

Controllers

translate requests

Repositories

implement ports

Presenters

shape output

Frameworks and drivers

Web framework

Database

External providers

RingContainsTest for membership
EntitiesRules that would be true even if this application did not existWould a paper-based version of the business still obey this rule?
Use casesThe steps of one application operation, and the ports it needsIs this a thing a user or a job asks the system to do?
Interface adaptersTranslation between the outside world's shapes and the inside'sDoes it exist only because of a specific delivery or storage technology?
Frameworks and driversEverything you did not write and cannot controlWould replacing it be a purchasing decision rather than a design one?
What lives in each ring, and the question that decides whether something belongs there.

How data crosses a boundary

The rule has an immediate practical consequence that trips up most first implementations. A use case cannot return a database row, because that type belongs to an outer ring. It also should not return an entity to a controller if that hands the outside world a mutable piece of the domain. So data crosses ring boundaries as simple structures owned by the inner ring — plain values with no behaviour and no framework annotations.

The use case owns its input and output shapes. The controller adapts to them, never the reverse.typescript
// use case ring — owns the port and the data shapes crossing its boundary.
export interface EnrolmentRepository {
  activeEnrolmentCount(learnerId: LearnerId): Promise<number>;
  save(enrolment: Enrolment): Promise<void>;
}

export interface EnrolInput { readonly learnerId: string; readonly courseId: string; }
export interface EnrolOutput { readonly enrolmentId: string; readonly startsOn: string; }

export class EnrolLearner {
  constructor(
    private readonly enrolments: EnrolmentRepository,
    private readonly clock: Clock,
  ) {}

  async execute(input: EnrolInput): Promise<EnrolOutput> {
    const learnerId = LearnerId.parse(input.learnerId);
    const active = await this.enrolments.activeEnrolmentCount(learnerId);

    // The entity holds the rule; the use case holds the sequence.
    const enrolment = Enrolment.open(learnerId, CourseId.parse(input.courseId), active, this.clock.now());
    await this.enrolments.save(enrolment);

    return { enrolmentId: enrolment.id.toString(), startsOn: enrolment.startsOn.toISOString() };
  }
}

// adapter ring — knows about HTTP and about the use case. The use case knows
// nothing about HTTP, which is why the same use case serves a CLI and a job.

Onion and clean: the differences worth knowing

Onion architecture

Palermo's formulation: a domain model core, domain services around it, application services around those, and infrastructure at the edge.

  • Emphasises a rich domain model at the centre
  • Maps naturally onto domain-driven design vocabulary
  • Fewer prescribed ring names to argue about
  • Less explicit about how data crosses boundaries
  • The distinction between domain services and application services confuses teams

Choose when: You are already working with domain-driven design and want a structural expression of it.

Clean architecture

Martin's formulation: entities, use cases, interface adapters, frameworks — with explicit rules about crossing boundaries and about which side owns an interface.

  • Explicit about boundary-crossing data structures
  • The use case is a first-class, named unit of application behaviour
  • Gives clear guidance on interface ownership
  • The full presenter-and-output-port machinery is heavy for most systems
  • Frequently applied literally, producing ceremony without benefit

Choose when: You want the discipline named and enforceable, and you are willing to drop the parts — presenters in particular — that your delivery mechanism does not need.

A use case needs to know whether the current user is an administrator. Where does that information come from without breaking the dependency rule?Reveal

It is passed in, as data the use case defines. The use case declares what it needs — a principal value with the permissions relevant to this operation — and an outer-ring adapter resolves the session, builds that value and hands it over. What it must not do is call a framework session helper from inside the use case, because that inverts the dependency and makes the use case unusable from a background job or a test. Notice that this is the same move as a repository port: the inner ring names the need, the outer ring satisfies it. Once you see that, most boundary questions answer themselves.

Key takeaways

  • Clean, onion and hexagonal are one idea: source dependencies point inward, control flows outward at runtime.
  • The centre must compile and be testable with every outer ring deleted.
  • Data crosses boundaries as simple structures owned by the inner ring, never as ORM rows or entities handed outward.
  • Framework annotations on entities are the most common invisible violation of the rule.
  • The differences between the named variants are far smaller than the differences between doing this well and doing it as ceremony.