skip to content

Why does building a stand-in as a subtype of a concrete chat-session class run that class's constructor, and what can that break?

level: seniorimportance: should knowfreq 38%

answer

  1. a subtype is an instance too
  2. the chain runs from the root
  3. counts come out exactly double
  4. fields inherited then never used
  5. make the stand-in be the object

basics

~20 s

Creating a subtype instance creates an instance of the class it extends, so the constructor chain usually runs. Every side effect inside it - a connection opened, a registration, a counter advanced - happens a second time, once for the target and once for the stand-in.

solid answer

~40 s

A generated subtype is a subtype, and instantiating it instantiates the class it extends: the constructor chain runs from the root down, unless the runtime offers an allocation path that skips constructors. If that constructor does work - opens a connection, registers the object somewhere, advances a counter, writes a row - the work happens once per stand-in created, on top of the real target's own construction. The stand-in also inherits every field the class declares; where it delegates to a separately built target, those fields are initialised and then never used, so code reading a field directly rather than through an overridden member reads the unused copy. The clean fix is to stop building two objects: let the stand-in be the object and call the inherited body.

code

pseudocode · 10 lines
pseudocode
class ChatSession(room) {
    connection = openConnection(room)   // a side effect in the constructor
    registry.add(self)
}

target  = construct ChatSession("room-7")        // connection 1, registry entry 1
standIn = synthesizeExtending(ChatSession, handler)
// the subtype's construction runs ChatSession's constructor again:
//   connection 2, registry entry 2 - both belong to the stand-in
// handler forwards every call to target, so connection 2 is never used

go deeper

for a junior

Recall that a stand-in built as a subtype is a real instance of the class it extends, so creating one is a second creation of that class, not a lightweight wrapper.

for a middle

Explain the construction chain: instantiating the subtype runs the class's constructors from the root down, and the stand-in inherits a full set of fields alongside the target's.

for a senior

Diagnose it from production symptoms - doubled registrations and connection counts for intercepted objects only - and pick a fix: side-effect-free constructors, or one object instead of two.

for a principal

Set the rule that makes it a non-issue across teams: constructors assign fields and nothing else, with connecting and registering in an explicit start step.

## Why the constructor runs at all A generated subtype is not a wrapper that happens to look like the class. It **is** an instance of that class, and the type system knows it because the subtype relation was declared when the type was built. Creating an instance of it therefore walks the same construction path as creating an instance of the class directly: the chain of constructors from the root of the hierarchy down to the generated type, in that order. There is one escape hatch worth knowing about. Some runtimes expose a way to **allocate storage for an object without running any constructor**, leaving every field at its default value. Where it exists, generation machinery often uses it. It is safe only for the delegating shape, because the stand-in's own fields are then meaningless and every call must go to the target through an overridden member. ## What runs a second time Count it honestly. For one logical object you asked to intercept, the delegating shape performs **two** constructions of that class: the real target, plus the stand-in. Anything the constructor does, it does twice: - opens a connection, a file handle or a pooled resource; - registers the new object with a registry, a listener list or a metrics collector; - advances a counter, an identifier generator or a sequence; - performs validation or expensive precomputation; - emits a record - a log line, an audit entry, a message. The visible symptoms rarely point at the stand-in: | Symptom | Cause | |---|---| | Two entries per object in a registry census | The constructor registers, and it ran twice | | Connection count exactly double the object count | Each stand-in opened one it never uses | | Identifiers advancing at twice the expected rate | A generator called from the constructor | | Creation fails for an exclusive resource | The second construction cannot acquire what the first took | | A metric counting creations that no request explains | The stand-in's construction is invisible in the call path | ## The state that comes with it Inheritance brings fields as well as members. The generated subtype declares the class's fields because it is one of them, and its own construction initialises them. In the delegating shape that copy is **initialised and then never used**: interception sends every call to the target, whose fields are the live ones. The copies diverge silently from the moment the target's state changes. ## The ways out 1. **Make the stand-in the object.** Create it through the class's own construction path with the real arguments, intercept in the handler, then call the **inherited** member body on that same instance. One construction, one set of fields, and the whole problem disappears rather than being managed. 2. **Move side effects out of constructors.** Keep construction to assigning fields and put connecting, registering and counting into an explicit start step that the stand-in's creation does not call. This is good design independently of stand-ins, and it makes the second construction harmless. 3. **Allocate without constructing**, where the runtime offers that path - accepting that the inherited fields hold defaults, which is safe only while the stand-in delegates everything. 4. **Use a contract-only stand-in instead.** Its synthesized type is not a subtype of that class, so none of that class's constructors are in its chain at all. ## Why the contract-only kind is immune A contract-only stand-in is built from declarations, not from a class. Its supertype is the contract; its only state is the reference to its handler; it runs nothing belonging to the target's class because it is not related to it. The target, if there even is one, is constructed exactly once, by whoever asked for it. This is one of the strongest practical arguments for arranging call sites around contracts: it removes an entire failure mode rather than mitigating it. ## How to diagnose it The tell is **counts that are exactly double** for objects that are intercepted and correct for objects that are not. Intercepted and plain objects sitting side by side in the same process make the comparison easy. The second tell is a constructor with a side effect you can name: read the construction path of the class from the root down and ask, for each statement, what happens if this runs once more for an object nobody will ever call. If the honest answer is not *nothing*, the delegating shape is unsafe for that class and one of the four ways out is required before interception ships.

  • The constructor opens a connection you cannot make repeatable - what are your options?
    Move the connect out of the constructor into an explicit start step the stand-in's creation never calls; or allocate the stand-in without running a constructor where the runtime offers that, accepting default-valued fields and full delegation; or drop the subtype route and use a contract-only stand-in, which constructs nothing belonging to that class.
  • Why does this hazard not arise for a contract-only stand-in?
    Its synthesized type declares the contract and is not a subtype of the target's class, so none of that class's constructors are in its construction chain. Its only state is its handler reference. The target, if one exists, is constructed once by whoever asked for it.
  • How would you confirm this is what you are seeing, rather than a bug in the handler?
    Compare counts for intercepted objects against equivalent objects that are not intercepted in the same process. A constructor side effect running twice shows as exactly double for the intercepted ones and correct for the rest, and it happens at creation, before any call reaches the handler at all.

saying these in an interview costs you the question

  • Thinks creating a subtype-based stand-in skips the parent constructor entirely
  • Believes the stand-in shares the target's fields rather than owning its own
  • Says constructor side effects are harmless because every call is delegated
  • Blames the handler for doubled registrations that happen before any call
  • Treats the duplicate state as a caching problem to be synchronised