---
title: 'Service Discovery and Gateway'
source: 'https://academia.sh/en/courses/enterprise-application-framework/service-discovery-and-gateway'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "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'
---

# 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.

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.

```java
// Service.java — common interface for every target that can be called (helper source, no main)
interface Service { String respond(); }
```

```java
// InventoryServiceV1.java — one target implementation (helper source, no main)
class InventoryServiceV1 implements Service {
    public String respond() { return "inventory-v1"; }
}
```

```java
// InventoryServiceV2.java — a second implementation of the same logical name (helper source, no main)
class InventoryServiceV2 implements Service {
    public String respond() { return "inventory-v2"; }
}
```

```java
// 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"; }
}
```

```java
// 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); }
}
```

```java
// 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();
    }
}
```

```java
// 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);
    }
}
```

```java
// 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`.

```java
// 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"); }
}
```

```java
// 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.
