Skip to content
academia.sh

Lesson 18 / 19

Service Discovery and Gateway

Who determined a call's target — a fixed reference in the source, a registry, or the container's own default resolution? Discovery and routing are modeled, there is no real network call. The limiting measurement measures the difference between discovery finding a target and verifying that the target still responds — a dead target never removed from the registry is found in a lookup the same way, the defect drops only at call time.

Published

In this lesson

  • Separates, by running it, whether a call's target comes from a fixed reference in the source, from a registry, or from the container's default resolution
  • Shows the difference between a registry finding a target and verifying that the target still responds
  • Measures that a dead target never removed from the registry is found in a lookup but raises an error when called
Contents

The previous lesson measured which component a value reaches. This lesson asks which target a call reaches. When one component wants to call another, where does it find that target? Three paths are modeled: a direct reference written fixed in the source, a target read from a registry, and the container’s own default resolution, which kicks in when nothing is found in the registry. The concepts of service discovery and gateway were built in the Service Architectures course and are not repeated here; this lesson measures only where the target comes from in the source. There is no real network call — several classes implementing the Service interface are wired together with direct Java calls — but the question asked is the same as in a real distributed system: who chose the target, and is that target still there?

In a real distributed system a target’s “address” is a hostname and a port, and both of these can change over time — one replica shuts down, a new one opens at a different address. This lesson models that variability not with a hostname but with a Java object: what ServiceRegistry holds is not a network address but a Service instance directly, but the question it asks is the same — what does a given name correspond to, and can that correspondence change over time?

Who Determined the Target: Direct Reference, Registry, Default

Four classes implement the same Service interface: InventoryServiceV1, InventoryServiceV2, FallbackService, and DeadService, used in the section that follows. A ServiceRegistry maps names to service instances; a Gateway, given a name, looks at the registry first, and falls back to its own default service if the name is not there.

// Service.java — common interface for every target that can be called (helper source, no main)
interface Service { String respond(); }
// InventoryServiceV1.java — one target implementation (helper source, no main)
class InventoryServiceV1 implements Service {
    public String respond() { return "inventory-v1"; }
}
// InventoryServiceV2.java — a second implementation of the same logical name (helper source, no main)
class InventoryServiceV2 implements Service {
    public String respond() { return "inventory-v2"; }
}
// FallbackService.java — the container's default resolution for any target with no counterpart in the registry (helper source, no main)
class FallbackService implements Service {
    public String respond() { return "fallback"; }
}
// ServiceRegistry.java — registry mapping names to service instances (helper source, no main)
import java.util.*;

class ServiceRegistry {
    final Map<String, Service> entries = new HashMap<>();
    void register(String name, Service s) { entries.put(name, s); }
    Service find(String name) { return entries.get(name); }
}
// Gateway.java — router that, given a name, looks at the registry first and falls back to its default if it finds nothing (helper source, no main)
class Gateway {
    final ServiceRegistry registry;
    final Service defaultService;

    Gateway(ServiceRegistry registry, Service defaultService) {
        this.registry = registry;
        this.defaultService = defaultService;
    }

    String call(String targetName) {
        Service s = registry.find(targetName);
        return (s != null ? s : defaultService).respond();
    }
}
// Measurement.java — result-printing helper (helper source, no main)
class Measurement {
    static void show(String label, Object value) {
        System.out.printf("%-52s: %s%n", label, value);
    }
}
// WhoDeterminedTheTarget.java — the same call shape is shown getting its target from three separate sources
public class WhoDeterminedTheTarget {
    public static void main(String[] args) {
        Service direct = new InventoryServiceV1();
        Measurement.show("call with a direct reference", direct.respond());

        ServiceRegistry registry = new ServiceRegistry();
        registry.register("inventory", new InventoryServiceV2());
        Gateway gateway = new Gateway(registry, new FallbackService());

        Measurement.show("via the gateway, target in the registry", gateway.call("inventory"));
        Measurement.show("via the gateway, target not in the registry", gateway.call("billing"));
    }
}
call with a direct reference                        : inventory-v1
via the gateway, target in the registry             : inventory-v2
via the gateway, target not in the registry         : fallback

ST28. The target of a call made with a direct reference is fixed in the source — this is a call: the line new InventoryServiceV1() directly determines which implementation is used, and this decision is fixed the moment the WhoDeterminedTheTarget class is compiled. ST29. The target of a call made through the gateway comes from a registry; what is fixed in the source is only a name — "inventory" — not which object stands behind that name. gateway.call("inventory") never changes at all, even if the object placed in the registry changes from InventoryServiceV1 to InventoryServiceV2, and the same call reaches a different implementation. ST30. When a name with no counterpart in the registry is requested, the gateway falls back to the container’s own default service; this third path never appears in the source at all — "billing" not being registered cannot be understood without looking inside the Gateway.call method.

A direct reference is the fastest path but the most rigid: changing InventoryServiceV1 to InventoryServiceV2 requires changing the WhoDeterminedTheTarget class itself and recompiling. The registry adds an indirection layer — the calling side knows only a name and has no idea at all which class the target comes from — and the cost of this indirection is that finding the target now requires a map lookup. The default service offers a third option: even if the registry is missing an entry, the calling side gets a response instead of an exception, but in exchange it becomes impossible to tell from the source whether that response came from the real target or from the fallback.

None of the three paths is “more correct” than the others. The direct.respond() call carries no indirection at all, so it is the fastest and most predictable, but it is also the one most closed to change — there is no way to change the target without changing the source text. Calling through the registry removes this rigidity: ServiceRegistry’s contents can change during a run, and the line gateway.call("inventory") can be written once and never change again while still being routed to different targets. The default service offers a third trade-off — even with no registry entry at all, the calling side does not come away empty- handed, but the cost of this convenience is that “a response arrived” is no longer the same thing as “a response arrived from the correct target.”

The Limiting Measurement: Discovery Finds a Target, It Does Not Verify It

The only job of ServiceRegistry.find is to turn a name into an object; it has no information at all about whether that object is still working correctly. Looking at the registry class’s own source makes this plain: entries.get(name) is a map lookup, and reading from a map never calls the object it holds at all — it only checks whether that object exists. To see this, a deliberately broken target is added to the registry — DeadService, an implementation that always throws an exception when called, but was never removed from the registry; as far as the registry is concerned, the name billing is still a valid entry, exactly like inventory.

// DeadService.java — a target that sits in the registry but always fails when called (helper source, no main)
class DeadService implements Service {
    public String respond() { throw new IllegalStateException("target unreachable"); }
}
// DiscoveryDoesNotVerify.java — the registry says 'found' for both a healthy and a dead target the same way; the difference only comes out at call time
public class DiscoveryDoesNotVerify {
    public static void main(String[] args) {
        ServiceRegistry registry = new ServiceRegistry();
        registry.register("inventory", new InventoryServiceV1());
        registry.register("billing", new DeadService());

        Measurement.show("did the registry find the /inventory target", registry.find("inventory") != null);
        Measurement.show("did the registry find the /billing target (actually dead)", registry.find("billing") != null);

        Gateway gateway = new Gateway(registry, new FallbackService());
        try {
            gateway.call("billing");
            Measurement.show("was the call successful", "yes");
        } catch (IllegalStateException e) {
            Measurement.show("was the call successful", "no, " + e.getClass().getSimpleName() + " fell at call time");
        }
    }
}
did the registry find the /inventory target         : true
did the registry find the /billing target (actually dead): true
was the call successful                             : no, IllegalStateException fell at call time

The first two lines are identical: registry.find returns true for both the healthy and the dead target, because both correspond to a key in the map — the registry checks only a mapping, not the target’s liveness. ST31. Discovery finds a target but does not verify whether it still responds; a dead target never removed from the registry is found in a lookup the same way as a healthy one, the defect drops only at call time. Nothing tells anyone that the billing target is broken until Gateway.call throws an IllegalStateException on the third line — neither the registry, nor the gateway, nor the earlier find call. This is another of the course’s before-the-fact-silent defects: the discovery step passes successfully, and the defect drops one step later, at a completely unexpected moment.

This distinction matters in practice, because the moment a system appears “healthy” and the moment it is “actually working” are not necessarily the same moment. A structure like ServiceRegistry can only list registered names; asking whether the target behind a registered name is still standing is an entirely separate responsibility, and this lesson shows that this responsibility is absent from the find method. One way to close this gap is to keep the registry together with liveness information — attaching a “last responded” field next to each target and writing a separate method that returns only targets whose information is current, instead of find. This lesson does not build that structure; it measures only the consequence of its absence, because the point of a limiting measurement is not to show the fix but to count what happens when it is not made.

The three sections’ measurements can be gathered into a single table:

Question Answer Its counterpart in the source
Who determined the target (direct reference) InventoryServiceV1 new InventoryServiceV1() — a call
Who determined the target (gateway) whatever is in the registry the name "inventory" — a registry
If an unregistered name is requested FallbackService no line at all — a default
Does find("billing") find the target yes, true discovery looks only at the mapping
Is call("billing") successful no, an exception liveness is only checked at call time

The last two rows of the table give different results for the same target — this is the lesson’s summary: whether a target exists and whether that target is working are two separate questions, and discovery answers only the first.

Summary

  • The target of a call made with a direct reference is fixed in the source; this is the case where the source of the behavior is a call.
  • The target of a call made through the gateway comes from a registry; what is fixed in the source is only a name, not which object corresponds to that name.
  • When a name with no counterpart in the registry is requested, the gateway falls back to the container’s own default service; this path never appears in the source at all.
  • Discovery finds a target but does not verify whether it still responds; a healthy and a dead target are found in a lookup the same way.
  • The defect of a dead target never removed from the registry drops not at discovery time but at call time.

Next Step

This lesson measured whether a call reached its target, and if it did, when it failed. The next lesson treats this failure not as a single data point but as a repeating event: after how many consecutive failures should the calling side stop trying, and where does that decision come from? The closing of the course and of the M08 curriculum measures this final boundary.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close