SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

System DesignDNS & Service Discovery
System Designbeginner

DNS & Service Discovery

How clients and microservices find each other — from public DNS resolution to internal service registries (Consul/ZooKeeper), with a runnable Python DNS server and Service Registry implementation.

January 4, 202412 min read
dnsservice-discoverymicroservicesnetworkingbuilding-blocktier-0

Metadata

FieldValue
Slugdns-service-discovery
DifficultyBeginner
Estimated Reading Time10 min
Estimated Coding Time15 min
Tier0 — Networking Fundamentals
Implementation LanguagePython
SEO DescriptionLearn how DNS and Service Discovery work in distributed systems. Understand hierarchical DNS resolution, A-records, CNAMEs, and internal service registries like Consul or ZooKeeper.

1. Overview

What problem does it solve?

Computers communicate using IP addresses (192.168.1.5 or 2607:f8b0:4005:808::200e), but humans and code use names (google.com or payment-service).

  • DNS (Domain Name System) solves this for the public internet, acting as the phonebook of the web.
  • Service Discovery solves this for internal microservices, dynamically tracking which IP addresses belong to which services as containers spin up and down.

What breaks without it?

  • Users would have to type IP addresses into their browsers.
  • If a server's IP address changes (which happens constantly in cloud environments), clients would break.
  • Microservices wouldn't know how to talk to each other in a dynamic environment like Kubernetes.
  • Load balancers wouldn't know which backend instances are healthy and alive.

2. Motivation

Why was DNS invented?

In the early ARPANET days (1970s), every computer downloaded a single text file called HOSTS.TXT from the Stanford Research Institute. As the internet grew to thousands of computers, maintaining a centralized file became impossible. Paul Mockapetris invented DNS in 1983 as a hierarchical, distributed database to replace HOSTS.TXT.

Why was Service Discovery invented?

DNS is heavily cached and relatively slow to update. In modern microservices (especially with Docker and Kubernetes), instances of a payment-service might be created and destroyed every few minutes. Standard DNS caching (TTL) would route traffic to dead IPs. Internal Service Discovery (like Consul, Eureka, or ZooKeeper) was invented to provide real-time, strongly consistent mapping of service names to healthy IP addresses.


3. Real-World Usage

SystemTechnology UsedUse Case
Public InternetRoute 53, CloudflareMapping sourav-saha.in to an IP
KubernetesCoreDNS + etcdMapping service.namespace.svc.cluster.local to Pod IPs
Uber / NetflixEureka (Netflix OSS)Internal microservices finding each other
HashiCorp stackConsulService mesh discovery and health checking
Kafka / HadoopApache ZooKeeperBroker discovery and leader election

4. Prerequisites

ConceptBlock
IP Addresses & Network Layers001 HTTP & TCP/IP Fundamentals

5. Visual Explanation

The Public DNS Resolution Flow

When you type www.example.com:

sequenceDiagram
    participant C as Client (Browser)
    participant R as Recursive Resolver (ISP/8.8.8.8)
    participant Root as Root Server (.)
    participant TLD as TLD Server (.com)
    participant Auth as Auth Server (example.com)

    C->>R: 1. What is the IP for www.example.com?
    R->>Root: 2. Where is .com?
    Root-->>R: 3. Ask TLD Server at IP 1.2.3.4
    R->>TLD: 4. Where is example.com?
    TLD-->>R: 5. Ask Auth Server at IP 5.6.7.8
    R->>Auth: 6. What is the IP for www.example.com?
    Auth-->>R: 7. The IP is 93.184.216.34 (A Record)
    R-->>C: 8. The IP is 93.184.216.34 (Cached for 1 hour)

Internal Service Discovery (Client-Side)

sequenceDiagram
    participant API as API Gateway
    participant SR as Service Registry (Consul)
    participant P1 as Payment Service (IP: 10.0.0.1)
    participant P2 as Payment Service (IP: 10.0.0.2)

    P1->>SR: Register("payment-service", 10.0.0.1)
    P2->>SR: Register("payment-service", 10.0.0.2)
    
    Note over P1,SR: Services send heartbeats every 10s
    
    API->>SR: Lookup("payment-service")
    SR-->>API: [10.0.0.1, 10.0.0.2]
    
    API->>P1: HTTP POST /charge (Client-side load balancing)

6. Internal Working

6.1 DNS Record Types

DNS holds different types of records, not just IPs:

TypePurposeExample
AMaps a name to an IPv4 addressexample.com → 93.184.216.34
AAAAMaps a name to an IPv6 addressexample.com → 2606:2800:220:1...
CNAMEMaps an alias to another namewww.example.com → example.com
MXMail exchange (where to send email)example.com → mail.google.com
TXTText data (often used for verification)example.com → "v=spf1 include:_spf..."
NSNameserver (delegates a subdomain)api.example.com → ns1.aws.com

6.2 DNS TTL (Time To Live)

Every DNS record has a TTL (e.g., 3600 seconds).

  • High TTL (24 hours): Less load on DNS servers, faster for users, but if your server IP changes, users will be broken for up to 24 hours.
  • Low TTL (60 seconds): Great for fast failover and load balancing (DNS Round Robin), but higher latency as clients query DNS more often.

6.3 Service Discovery Mechanisms

Inside a datacenter, DNS caching is too slow. Service Discovery provides real-time state.

1. Service Registration: When a service boots up, it registers its IP and Port with the Registry. 2. Health Checking: The Registry (or the service) sends periodic heartbeats. If a service dies, it is evicted from the registry immediately. 3. Service Discovery:

  • Client-side: The client asks the registry for IPs and picks one (e.g., Netflix Ribbon).
  • Server-side: The client talks to a Load Balancer, and the Load Balancer queries the registry (e.g., AWS ALB, Nginx).

7. Implementation

Why Python? We will implement a simplified internal Service Registry over HTTP, showing exactly how microservices register themselves, send heartbeats, and how clients discover them.

"""
004 - Service Registry & Discovery
A simplified internal "Consul" or "Eureka".
Services register via POST, send heartbeats, and are automatically 
evicted if they die. Clients query via GET.
"""
import time
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
from urllib.parse import urlparse

# ── The Registry State ──

# Format: {"service_name": {"ip:port": last_heartbeat_timestamp}}
registry: dict[str, dict[str, float]] = {}
registry_lock = threading.Lock()

# How long before a service is considered dead (seconds)
TTL = 15 


# ── The Server ──

class ServiceRegistryHandler(BaseHTTPRequestHandler):
    def _send_json(self, status: int, data: dict):
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode("utf-8"))

    def do_POST(self):
        """Register or Heartbeat a service instance."""
        parsed_path = urlparse(self.path)
        
        # Path format: /register/<service_name>
        if parsed_path.path.startswith("/register/"):
            service_name = parsed_path.path.split("/")[-1]
            
            content_length = int(self.headers.get("Content-Length", 0))
            body = json.loads(self.rfile.read(content_length))
            instance_id = body.get("instance") # e.g. "10.0.0.1:8080"
            
            if not instance_id:
                self._send_json(400, {"error": "Missing 'instance' in body"})
                return

            with registry_lock:
                if service_name not in registry:
                    registry[service_name] = {}
                
                # Update the heartbeat timestamp
                registry[service_name][instance_id] = time.time()
                
            print(f"[REGISTER/HEARTBEAT] {service_name} -> {instance_id}")
            self._send_json(200, {"status": "registered", "ttl": TTL})
        else:
            self._send_json(404, {"error": "Not Found"})

    def do_GET(self):
        """Discover instances for a service."""
        parsed_path = urlparse(self.path)
        
        # Path format: /discover/<service_name>
        if parsed_path.path.startswith("/discover/"):
            service_name = parsed_path.path.split("/")[-1]
            
            with registry_lock:
                instances = registry.get(service_name, {})
                # Filter out dead instances on the fly
                now = time.time()
                healthy_instances = [
                    inst for inst, last_beat in instances.items()
                    if now - last_beat <= TTL
                ]
            
            self._send_json(200, {
                "service": service_name,
                "instances": healthy_instances
            })
        else:
            self._send_json(404, {"error": "Not Found"})

    # Suppress default HTTP logging to keep console clean
    def log_message(self, format, *args):
        pass


# ── Background Eviction Task ──

def evict_dead_services():
    """Periodically clean up instances that missed their heartbeats."""
    while True:
        time.sleep(5)
        now = time.time()
        with registry_lock:
            for service_name, instances in list(registry.items()):
                dead = [inst for inst, last_beat in instances.items() if now - last_beat > TTL]
                for d in dead:
                    del registry[service_name][d]
                    print(f"[EVICTED] {service_name} -> {d} (Missed heartbeat)")
                
                # Clean up empty services
                if not registry[service_name]:
                    del registry[service_name]


if __name__ == "__main__":
    # Start the eviction thread
    threading.Thread(target=evict_dead_services, daemon=True).start()

    port = 8500
    server = HTTPServer(("127.0.0.1", port), ServiceRegistryHandler)
    print(f"Service Registry running on port {port}...")
    print("Test via:")
    print("  curl -X POST http://127.0.0.1:8500/register/payment -d '{\"instance\":\"10.0.0.1:8080\"}'")
    print("  curl http://127.0.0.1:8500/discover/payment")
    server.serve_forever()

Sample Output

# Terminal 1: Run Registry
$ python registry.py
Service Registry running on port 8500...

# Terminal 2: Start Service Instance A
$ curl -X POST http://127.0.0.1:8500/register/payment \
    -d '{"instance":"10.0.0.1:8080"}'
{"status": "registered", "ttl": 15}

# Terminal 2: Start Service Instance B
$ curl -X POST http://127.0.0.1:8500/register/payment \
    -d '{"instance":"10.0.0.2:8080"}'

# Terminal 3: API Gateway queries discovery
$ curl http://127.0.0.1:8500/discover/payment
{"service": "payment", "instances": ["10.0.0.1:8080", "10.0.0.2:8080"]}

# Wait 15 seconds without sending heartbeats...
# Terminal 1 outputs:
[EVICTED] payment -> 10.0.0.1:8080 (Missed heartbeat)
[EVICTED] payment -> 10.0.0.2:8080 (Missed heartbeat)

# Terminal 3: Query again
$ curl http://127.0.0.1:8500/discover/payment
{"service": "payment", "instances": []}

8. Complexity

MetricDNSInternal Service Registry (Consul/ZooKeeper)
Read Latency~0ms (cached locally)~1-5ms (network call to registry)
Write LatencySlow (propagation takes minutes/hours)Fast (<100ms)
ConsistencyEventual (due to TTL caches)Strongly Consistent (usually backed by Raft)
ProtocolUDP (mostly)HTTP/TCP or gRPC

Scalability Characteristics

  • DNS Scaling: Scales massively through hierarchical caching. The Root servers handle almost no traffic because your ISP and your OS cache the results.
  • Service Registry Scaling: Usually deployed as a 3 or 5 node cluster using a consensus algorithm like Raft (Block 020). The clients maintain local caches of the registry state to reduce load on the cluster.

9. Trade-offs

Client-Side Discovery vs Server-Side Discovery

FeatureClient-Side (Netflix Ribbon)Server-Side (AWS ALB, K8s Services)
ArchitectureClient queries registry, picks IPClient calls Load Balancer, LB queries registry
Network Hops1 hop (Direct to service)2 hops (Client → LB → Service)
Language CouplingHigh (Client needs discovery logic)Low (Client just makes an HTTP call)
ComplexityDistributed logic across all clientsCentralized at the Load Balancer

Modern trend: The Service Mesh (Block 034) extracts client-side discovery out of the application code and puts it into a sidecar proxy (Envoy), combining the best of both worlds.


10. Production Evolution

ConcernThis ImplementationProduction (Consul / CoreDNS)
State StorageIn-memory python dictDistributed KV store using Raft for consensus
Health ChecksTTL expiration onlyActive TCP/HTTP pinging, CPU/Memory checks
High AvailabilitySingle Point of FailureMulti-node cluster with Leader Election
NotificationsPollingLong polling or gRPC streams to push updates to clients
DNS InterfaceHTTP onlyExposes a standard DNS port (53) for legacy apps

11. Common Bugs

BugWhat happensFix
DNS TTL too highMigration fails because clients cache the old IP for 24 hoursLower TTL to 60s 48 hours before the migration
Missing HeartbeatsHeavy CPU load causes service to miss a heartbeat; it gets evicted falselyUse separate threads for heartbeats, or active health checks
Thundering HerdRegistry goes down, comes back up, 10,000 services instantly registerAdd jitter to registration retries
Network PartitionRegistry is isolated from services, evicts them all, bringing down the appImplement "Panic Mode": if 80% of services fail at once, stop evicting (assume registry network is at fault)

12. Interview Questions

  1. What happens if the Root DNS servers go down? Hint: Nothing immediately. TLDs and Auth servers are heavily cached by ISPs.

  2. Why don't we use standard DNS for internal microservice discovery? Hint: DNS relies on TTL caching. In a dynamic environment, you can't afford to route traffic to a dead container for 60 seconds.

  3. What is a CNAME record and why can't the root domain (example.com) be a CNAME? Hint: CNAME maps a name to a name. DNS RFCs forbid CNAMEs at the apex because it conflicts with NS and MX records.

  4. In your service registry, how do you prevent a temporary network blip from causing a service to be evicted? Hint: Adjust the TTL, use multiple missed heartbeats as a threshold, or use active health checking.


13. Used By (Downstream Blocks)

  • 007 Load Balancer — LBs use discovery to find backend IPs
  • 012 CDN — CDNs use DNS CNAMEs and Anycast routing
  • 027 Heartbeat & Failure Detection — Core mechanism of service registries
  • 034 Service Mesh — Relies heavily on internal service registries

14. Used In (Case Studies)

SystemDNS / Discovery Strategy
TinyURLUses DNS to map short domain to API Gateway
NetflixCreated Eureka for massive AWS service discovery
KubernetesUses CoreDNS internally to map service names to Pods
UberUses Hyperbahn / internal registries for microservice routing
KafkaBrokers register themselves with ZooKeeper (discovery)

15. Related Blocks

RelationshipBlock
Previous001 HTTP & TCP/IP Fundamentals
Next007 Load Balancer
Next020 Quorum & Consensus (Raft) — how Consul stays highly available

16. Try It Yourself

Exercise 1: Client-Side Load Balancer

Write a python script that acts as an API Gateway. It should call GET /discover/payment, parse the JSON, and then randomly select one of the instances from the list to "route" the request to.

Exercise 2: Active Health Checks

Instead of waiting for services to POST a heartbeat, modify the registry to maintain a list of /health endpoints for registered services. Have the eviction thread actively send an HTTP GET to those endpoints every 10 seconds.


Website Metadata

FieldValue
Hero TitleDNS & Service Discovery
Hero SubtitleHow clients find servers, and how microservices find each other in dynamic cloud environments
BreadcrumbSystem Design → Building Blocks → DNS & Service Discovery
Sidebar CategoryTier 0 — Networking Fundamentals
Search Keywordsdns, service discovery, consul, zookeeper, eureka, cname, a record, microservices routing, client side load balancing
Internal Links← 001 HTTP & TCP/IP · → 007 Load Balancer
Suggested IllustrationA switchboard operator holding a giant phonebook (DNS) connecting wires, contrasted with a digital live-updating radar screen (Service Discovery)
Suggested AnimationA microservice spinning up, registering its IP on a central board, sending regular heartbeats, then dying and being erased from the board
PreviousHashing & Hash FunctionsNextWebSockets & Long Polling