One class reads and writes another class's fields directly instead of going through its methods. Name that kind of coupling, and explain why whether a compiler can even prevent it depends on the language's unit of protection — compare Python, Go, Rust, C++ and Java, including what Java's reflective `setAccessible(true)` still does at runtime on current JDKs.
answer
- content = depends on representation, breaks on any change
- unit of protection: type / package / module / none
- Python `__x` mangles to `_Cls__x` — anti-collision, not access control
- Java: class at compile time, module (if any) at runtime
- C++ friend = invited; protected data = unbounded
basics
~20 sContent coupling — the strongest kind, because the dependent breaks on any internal change. Whether a compiler can stop it depends on the language's unit of protection: per-class in Java, C# and C++, per-package in Go, per-module in Rust, and convention-only in Python.
solid answer
~60 sIt is **content coupling**: the dependent relies on another unit's internal representation rather than its published surface, so any representation change is a breaking change. What counts as *internal* is set by the language's protection unit. - **Python**: none — `__x` merely name-mangles to `_Cls__x`, still reachable from anywhere. - **Go**: no per-type visibility; capitalisation exports from a *package*, so two types in one package touch each other's fields by design. - **Rust**: the module — fields are private to the defining module and its descendants, with `pub(crate)`/`pub(super)` to state the blast radius. - **C++**: granted explicitly by `friend`; `protected` data hands internals to every present and future subclass. - **Java**/**C#**: per class at compile time, but reflection reopens it at runtime. On JDK 17, `setAccessible(true)` still succeeds for classpath code in the unnamed module; only a named module that withholds `opens` (or a JDK-internal package) makes it throw. So Java's compile-time unit is the class, and its runtime unit — where there is one at all — is the module. Classify against the unit, not the keyword.
code
text · 14 linesunit = TYPE (Java, C#, C++)
class A { private f }
class B { read A.f } -> rejected at compile time
unit = PACKAGE (Go)
package p: type A{f}; type B; B reads A.f -> legal, idiomatic
package q: reads p.A.f -> rejected (unexported)
unit = MODULE (Rust)
mod m { struct A{f}; fn peek(a:&A) -> a.f } -> legal in m + children
outside m: a.f -> rejected
unit = NONE (Python)
obj._Cls__f -> legal, just mangledgo deeper
Name content coupling and state the test: the dependent breaks whenever the owner changes internals, even when behaviour is unchanged. Knowing that private exists and why direct field access is discouraged is enough.
Expected to give the classification plus the unit-of-protection comparison: class in Java/C#/C++, package in Go, module in Rust, convention in Python — and to be precise that Python's __x mangles rather than hides.
Add the runtime dimension: Java enforces per class at compile time but per module at runtime, and only for code that actually publishes modules, which is why reflective frameworks still populate private fields on classpath code. Connect it to leaks like returning mutable internal collections and field-bound serialisation.
Frame it as choosing the design unit you are willing to defend. Argue when a wider unit is correct (a cohesive Go package or Rust module written as one design), when it is a smell, and how the protection unit should drive package/module layout, library API surface and refactoring blast radius rather than the other way round.
## The classification **Content coupling** sits at the top of the coupling severity scale: unit A depends on the internal representation of unit B — its fields, its layout, its private helpers — rather than on the interface B publishes. It is the worst kind because the set of changes that break the dependent is the set of *all* changes, including ones the owner rightly considers invisible: renaming a field, collapsing two fields into one computed value, or swapping a list for a map. The cleanest test is a thought experiment. If the owner rewrote the internals but preserved the observable behaviour of every published method, would the dependent still compile and still be correct? If not, the dependency is on content, not on contract. ## Why the boundary is not universal The interesting follow-up is: *what counts as internal?* Every language answers with a **unit of protection**, and the units genuinely differ — which means the same source shape is a defect in one language and idiomatic in another. **Python: no enforcement, only signal.** A single leading underscore is a documented convention meaning "not part of the interface". A double leading underscore triggers name mangling — `self.__count` inside class `Widget` is stored as `_Widget__count` — and that mechanism exists to prevent *accidental collisions* between a class and its subclasses, not to prevent access. `obj._Widget__count` reads it from anywhere. Python's real defence is cultural, backed by a design trick: `@property` lets an owner start with a plain attribute and later replace it with a getter without changing any call site, so attribute-access *syntax* does not imply content coupling the way it does in a language with enforced fields. **Go: the package is the unit.** Go has no per-type field privacy at all. An identifier is exported from its package if it begins with a capital letter — that is the whole rule. Two types declared in the same package can freely read and write each other's unexported fields, and idiomatic Go does exactly that: a package is written as one design unit with a small exported surface. So in Go, content coupling is only a meaningful accusation *across* a package boundary; keeping packages small and purposeful is the discipline that replaces per-type privacy. **Rust: the module is the unit, with an explicit radius.** Struct fields are private to the module that defines them, and descendant modules can see their ancestors' private items. `pub(crate)`, `pub(super)` and `pub` let the author state exactly how far visibility should reach, turning blast radius into a documented decision rather than a binary. Rust also blocks a related runtime leak: handing out a mutable reference to internal state is visible in the signature and borrow-checked, rather than silently escaping. **C++: content coupling by invitation.** `friend` is content coupling the owner explicitly granted — defensible because it is enumerated and declared inside the owner's own class. `protected` data members are the opposite: they extend internal access to every subclass that exists now or will ever be written, by anyone. That is why the long-standing guidance is to keep data `private` and expose `protected` *functions* when subclasses need controlled access. **Java and C#: per class at compile time, per module (at most) at runtime.** `private` means private to the declaring class; Java additionally allows access between a nested class and its enclosing class (nestmates), which is a nuance of "one class" rather than an exception. The important correction most candidates get wrong is what reflection does today. `setAccessible(true)` followed by a field read still **succeeds** for ordinary application classes compiled to the classpath, on JDK 17 and later, with no command-line flags — classpath types live in the unnamed module, which is open by definition. That is precisely why reflective serializers and ORMs keep working on classpath code. Strong encapsulation (JEP 396 in JDK 16, made permanent by JEP 403 in JDK 17) closed illegal reflective access to **JDK internals** and to packages of **named modules** that do not declare `opens`. So the escape hatch was not closed; it was narrowed to module boundaries, and only for code that actually publishes a module. C# is similar in spirit: `private` is a compile-time rule, and reflection can reach past it. ## Consequences for classifying real designs Two practical rules follow. First, ask what unit the language protects **before** calling a field access a defect. A reviewer objecting to one Go struct touching another's unexported field inside the same package is applying a Java model to a language that never had one. Second, enforcement changes the failure mode, not the principle. Convention-only privacy still yields a working system if the team respects it; the cost is that violations surface in review or a linter instead of a compiler, and any consumer can pin you to your current representation. ## Related shapes to recognise - A getter and setter for every field is content coupling in a trench coat: the representation is still the contract. - Returning a mutable internal collection is content coupling handed out at runtime — callers mutate your state without calling your methods. - Serialisation frameworks bound directly to fields make the storage format a copy of your internals, so a refactor becomes a migration. ## The answer to give Name content coupling, give the breaks-on-any-internal-change test, then show you know that "internal" is drawn by the language: per class in Java, C# and C++, per package in Go, per module in Rust, by convention in Python — and that Java's runtime boundary is the module, not the class.
- A class exposes a getter and setter for every field. Has it avoided content coupling?Not in substance. If the accessor set is a one-for-one mirror of the fields, the internal representation is still the published contract, and changing it still breaks every caller. The accessors buy you one thing only: a seam where you can later insert computation or validation without editing call sites. Real decoupling comes from publishing operations that express intent (`applyDiscount`) rather than data (`getPrice`/`setPrice`).
- Two Go types in the same package read each other's unexported fields. Is that a coupling defect?Not by itself. Go's unit of protection is the package, so the language is telling you the package — not the type — is the design unit whose internals may be shared. The question to ask instead is whether the package is cohesive and its exported surface small. It becomes a defect when the package has grown into unrelated responsibilities, because then the shared-field access is silently coupling things that should have been separate packages.
- Does Java's module system stop frameworks from reflecting into private fields?Only across module boundaries. Application classes on the classpath live in the unnamed module, which is open by definition, so `setAccessible(true)` on their private fields still succeeds on JDK 17 with no flags — that is why reflective serializers and ORMs work unchanged on classpath code. Strong encapsulation (JEP 396/403) blocks reflection into JDK-internal packages and into packages of named modules that do not declare `opens`; those cases need an `opens` directive or an `--add-opens` launcher flag.
saying these in an interview costs you the question
- Saying Java's module system "closed" reflective field access — classpath classes are in the unnamed module, which is open, so `setAccessible(true)` still succeeds on JDK 17 with no flags; only named modules without `opens` and JDK-internal packages throw.
- Claiming Python's double underscore makes an attribute private — it only name-mangles to `_Cls__x`, which anyone can spell.
- Calling same-package field access in Go a coupling violation, which imports a per-type model Go never had.
- Treating a full set of one-for-one getters and setters as proof that content coupling was avoided.
- Saying `protected` data is the safe middle ground, when it exposes internals to every subclass that will ever be written — unlike C++ `friend`, which is enumerated by the owner.