Realization: implementing an interface
Open in the interactive app: practice, quiz, mock interview →In one minute
- Realization is the UML relationship between a specification and the class that fulfils it, usually a class implementing an interface. It is drawn as a dashed line with a hollow triangle pointing at the interface.
- Generalization (solid line, hollow triangle) passes state and code to the child; dependency (dashed line, open arrowhead) means one class uses another. Realization passes only obligations.
- In code it is
implementsin Java, C# and TypeScript,impl Trait for Typein Rust, and an ABC subclass in Python. Go has no declaration: the relationship exists whenever the method set fits, so teams add a compile-time assertion. - A realization is a claim. Compilers check signatures; only conformance tests check behaviour.
- At runtime each (class, interface) pair costs some dispatch data: a Java itable block, a Go itab, an extra vtable pointer per C++ object, or nothing in TypeScript, where
implementsis erased.
How it works
Three arrows that look alike
A payouts team moves money over several bank rails. PayoutService needs something that can send a payout and report its status; ACH, SEPA and card push each do it differently. In plain text:
PayoutService - - - - - - > <<interface>> PayoutRail (dependency: uses)
^ ^
: : (realization: dashed, hollow triangle)
AchRail SepaRail
|
| (generalization: solid, hollow triangle)
SepaInstantRailRead each arrow by what crosses it. Generalization from SepaInstantRail to SepaRail carries fields, method bodies and the obligation to behave like a SepaRail. Realization from SepaRail to PayoutRail carries only the obligation: provide send and status with these signatures and documented behaviour. Dependency carries nothing; it says a change to PayoutRail may force a change to PayoutService. UML's compact form shows the same thing: a "lollipop" on SepaRail means it provides PayoutRail, and a "socket" on PayoutService means it requires it.
Arrows point at the abstraction
In a well-layered design the dependency and all realizations point at PayoutRail. That is Dependency Inversion drawn as geometry, and a quick review test: an arrow from a high-level class to a concrete rail means the inversion has leaked.
What realization does and does not give you
A realizing class must provide every declared operation with compatible signatures, and gets no fields. Java default methods, Rust default trait methods and concrete methods on Python ABCs let an interface hand over some code, which blurs the line with generalization; keep anything holding state in a class.
A class may realize several role interfaces: SepaRail might realize PayoutRail, Cancellable and HealthReporter. Trouble starts when two interfaces use one name for different things, such as close() meaning "end the session" in one and "close the settlement batch" in the other. Java and TypeScript give the class one close; C# can provide both through explicit interface implementation.
Declared versus implicit realization
Nominal languages record the realization (class SepaRail implements PayoutRail), so the compiler checks it and IDEs list every realizer. In Go a type realizes PayoutRail the moment it has Send and Status with the right signatures. Nothing records intent, and a renamed method breaks the realization silently until some distant conversion fails. The Go FAQ's fix is one line per realizer: var _ PayoutRail = (*SepaRail)(nil).
TypeScript checks implements but accepts any object of the right shape and erases the clause from the emitted JavaScript. Python has two forms. Subclassing an abc.ABC blocks instantiation if an abstract method is missing. PayoutRail.register(SepaRail) declares a "virtual subclass" and checks nothing: isinstance returns true even if SepaRail has no send.
Diagrams that stay true
A hand-drawn class diagram is accurate on the day it is drawn. Diagram-as-code (Mermaid writes realization as PayoutRail <|.. SepaRail) can be generated from source or checked against it, so a missing realization fails a build instead of misleading a new hire. The practice task builds the core of such a check.
Reviewing agent-written code
Agents produce realizations that compile but do not conform: an implements PayoutRail whose cancel throws "not supported", a register() call added to make an isinstance check pass, or a generated diagram that draws interfaces with solid generalization arrows. Ask of each realization: is it declared or asserted, does a conformance test run against it, and does the diagram match the code?
The same shape elsewhere
Realization is the trust but verify shape: a declaration is a claim about behaviour. A service implementing an OpenAPI spec, a storage system claiming S3 compatibility and a driver claiming POSIX compliance all realize a specification written by someone else, and all need conformance suites, because "it compiles" and "it accepts the request" are the weakest evidence there is.
Down the stack
Layer 1The contract
The realizing type provides every member of the interface and its superinterfaces with compatible signatures, and its values are accepted where the interface is expected. Java and C# check at compile time unless the class is abstract; Go at each static conversion; TypeScript only at compile time; Python ABCs at instantiation. Separately compiled code can still break it: if an interface gains a method after a realizer was compiled, the JVM links the old class anyway and throws AbstractMethodError only when the missing method is called.
Layer 2The mechanism
Java. The class file's interfaces array names the directly implemented interfaces. When HotSpot links the class it builds an itable with an entry for every implemented interface, including superinterfaces and those inherited from superclasses, each pointing to a block of method pointers. Default methods are filled from the interface; missing ones get a stub that throws AbstractMethodError.
C++. Realizing an abstract class means inheriting from it. Each abstract base after the first adds its own vtable pointer inside the object, and calls through it use thunks that adjust this.
Go. A type's runtime descriptor lists its methods sorted by name, with no record of which interfaces it satisfies. The compiler emits an itab for each static (type, interface) conversion; type assertions build itabs lazily by merging the two sorted method lists and cache them.
TypeScript and Python. tsc checks implements and emits nothing. CPython's ABCMeta computes __abstractmethods__ at class creation, and register() adds the class to a weak registry consulted by isinstance, without touching its MRO.
Layer 3The machine
In Java realization costs metaspace, not object size: a class realizing four interfaces stores four itable entries and their method blocks once, and each instance keeps its 12 byte header (with compressed class pointers). A C++ object realizing four abstract bases holds four vtable pointers, 32 bytes per instance; across a million small objects that is 32 MB of pointers. A Go interface value is two words (16 bytes), and each itab is roughly 32 bytes plus 8 per method, allocated once per pair. A Python ABC isinstance on a class it has not seen walks the registry and subclass hooks, taking microseconds, then caches the answer per class.
Why it's built this way
- Callers should depend on what they need, not who provides it, so the specification is a separate type that providers realize.
- A class plays several roles, so it maps onto several interfaces, each needing its own method table or itable block.
- Building every possible table up front is wasteful, so Java builds itables for declared interfaces at link time and Go builds itabs only for pairs actually used.
- Signatures are cheap to check mechanically and behaviour is not, so compilers check shape and teams own conformance tests.
Diagram
flowchart TD S["class SepaRail implements PayoutRail"] --> J["javac: all methods present?"] J -- "no" --> E["compile error"] J -- "yes" --> CF["class file interfaces[]"] CF --> L["JVM link: build itable"] L --> IT["itable block for PayoutRail"] IT --> CALL["invokeinterface send"] G["Go type with Send, Status"] --> A["var _ PayoutRail = (*SepaRail)(nil)"] A --> T["compiler emits itab"] T --> CALL2["interface call via itab"]
Worked example
The payouts platform has five rails realizing PayoutRail and processes 1.2 million payouts a day. Product adds a required cancel(payout_id). Four rails live in the platform repository and fail to compile until cancel is implemented, which is the system working. The fifth, a card-push rail, is a vendor jar compiled against the old interface.
The JVM links it without complaint. About 0.4 percent of payouts are cancelled, 4,800 a day, and the vendor rail carries 15 percent of traffic, so roughly 720 cancellations a day hit AbstractMethodError, each leaving a payout in flight that the customer believes was cancelled.
The fix had three parts. cancel moved to a separate Cancellable interface, and the service returns "cannot cancel on this rail" when a rail does not realize it. A conformance suite (send, status, idempotent retries, cancel where realized) runs in CI against every class in the ServiceLoader configuration, vendor jars included. And the architecture diagram is now generated from code, so the vendor rail's missing realization arrow is visible.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
Declared (implements) | Intent recorded; tools find realizers | Foreign types need adapters | Java, C#, published service interfaces |
| Implicit (structural) | Foreign types fit; consumer owns interface | Intent invisible; silent drift | Go and TypeScript role interfaces |
| Implicit plus assertion | Structural with a build check | One line per pair | Every Go type meant to realize an interface |
Virtual registration (register) | Retrofit types you do not own | Checks nothing | Third-party classes, with tests |
| Default methods | Add methods without breaking realizers | Blurs realization and inheritance | Evolving a widely realized interface |
Pitfalls in production
- An interface gains a method and a prebuilt realizer throws
AbstractMethodErrorat runtime. Fix: add capabilities as new interfaces or default methods, and run conformance tests against binary plugins. - A Go type stops realizing an interface after a signature change and the error appears in an unrelated package. Fix:
var _ I = (*T)(nil)beside each realizer. - A
registered Python class lacks a method and fails withAttributeErrorfar from the cause. Fix: subclass the ABC, or test registered classes against the interface. - A realizer implements a method by throwing "not supported". Fix: it does not realize that interface; split the roles.
- Diagrams draw interfaces with generalization arrows, so reviewers assume shared state. Fix: generate diagrams from code.
Leadership lens
- Treat each realization of a shared interface as a claim needing evidence: a conformance suite owned by the interface's team and run by every realizer.
- In design review, ask where each interface lives and which way the arrows point. Consumer-owned interfaces and arrows into abstractions keep teams independent.
- For interfaces realized outside your repository (plugins, vendors, partner SDKs), set a compatibility policy: new methods are optional or defaulted for at least one release.
- Use diagrams generated from code for onboarding and audits; hand-drawn ones drift within a quarter.
Check yourself
How do generalization and realization differ in what the child receives?
Show answer
Generalization passes fields, method bodies and obligations. Realization passes only obligations, plus any default methods the interface supplies.
A Java service calls a new interface method on a plugin compiled last year and gets
AbstractMethodError. Why did class loading succeed?Show answer
HotSpot fills missing itable entries with a stub that throws when called, so the error appears at the first call, not at load time.
Why does a C++ class realizing four abstract interfaces make each object bigger, while the same design in Java does not?
Show answer
In C++ each extra base subobject carries a vtable pointer in the instance. In Java the itable lives in class metadata.
What does
PayoutRail.register(SepaRail)verify in Python?Show answer
Nothing about methods. It makes
isinstancereturn true; a missing method fails only when called.
Watch
- UML Class Diagram Tutorial (Lucid Software): the notation for every class relationship, realization included.
- Go Proverbs (Rob Pike, Gopherfest 2015): small, implicitly satisfied interfaces.
Free resources
- Class diagram (Wikipedia): the UML relationships side by side.
- Mermaid class diagram syntax: realization (
<|..) as diagram-as-code. - Go FAQ: How can I guarantee my type satisfies an interface?: the compile-time assertion idiom.
- Java Language Specification, Chapter 13: Binary Compatibility: what happens to compiled realizers when an interface changes.
- abc: Abstract Base Classes (Python docs): subclassing versus
register().