Skip to content

Instantly share code, notes, and snippets.

@InKolev
Last active March 15, 2026 15:39
Show Gist options
  • Select an option

  • Save InKolev/55c7f654239f7a3cc47c2c068eef8557 to your computer and use it in GitHub Desktop.

Select an option

Save InKolev/55c7f654239f7a3cc47c2c068eef8557 to your computer and use it in GitHub Desktop.

Building and Maintaining Resilient Services

A Practical Engineering Guide Best practices for designing, operating, and scaling production-grade distributed systems.


Table of Contents

  1. Service Scope and Boundaries
  2. API Design and Versioning
  3. Observability
  4. Resilience Patterns
  5. Caching Strategies
  6. Traffic Management
  7. Security
  8. Testing for Resilience
  9. Deployment and Release Strategy
  10. Documentation and Operational Runbooks

1. Service Scope and Boundaries

Every resilient system starts with a clear definition of what each service is responsible for. Poorly scoped services create cascading complexity: too broad and they become monoliths in disguise; too narrow and you drown in inter-service coordination.

1.1 Defining Responsibility

Single Responsibility Principle: Each service should own one well-defined business capability. A payment service processes payments. A notification service delivers notifications. When you find a service doing both, you have a candidate for decomposition.

The Two-Week Rule: A practical heuristic for size is that any individual service should be small enough that a competent engineer could rewrite it from scratch in approximately two weeks. This keeps cognitive load manageable and makes the service easy to reason about, test, and replace.

1.2 Avoiding Over-Partitioning

Resist the temptation to split every function into its own service. Over-partitioning introduces latency from excessive network calls, creates deployment coordination headaches, and can lead to distributed monolith anti-patterns where tightly coupled services must be deployed together. If two services cannot function independently, they likely belong together.

💡 Ask yourself: can this service be deployed, scaled, and reasoned about independently? If the answer is no, reconsider your boundaries.


2. API Design and Versioning

2.1 Present Your API Formally

Use a well-established specification format to describe your API contract. OpenAPI (formerly Swagger) is the industry standard. A machine-readable specification enables automated documentation generation, client SDK generation, contract testing, and a shared understanding between teams that eliminates guesswork.

2.2 Versioning Strategy

Never modify a published contract without giving consumers time to adapt. Breaking changes require a new API version. The most straightforward approach is a version prefix in the URL path:

Version Example Route Notes
v1 /api/v1/users/{id} Initial contract
v2 /api/v2/users?userId={id} Query parameter change
v3 /api/v3/users/{id}?fields=name,email Field selection support

2.3 Versioning Best Practices

  • Deprecation policy: Announce deprecation of old versions at least one release cycle in advance. Provide a clear migration guide and timeline.
  • Backward compatibility: Additive changes (new optional fields, new endpoints) do not require a version bump. Only breaking changes do.
  • Sunset headers: Return HTTP Sunset and Deprecation headers so consumers can detect when they are using an outdated version programmatically.

3. Observability

If you cannot see a system working, you do not know whether it is working. Observability is the combination of metrics, logs, and traces that gives you a real-time understanding of system health and behavior.

3.1 Metrics

Instrument every service with quantitative measurements. The following table lists the essential metrics every production service should expose:

Metric Why It Matters
Error count (by type) Distinguishes client errors, server errors, network faults, and dependency failures. Enables targeted alerting.
Throughput (requests/min) Reveals system load, identifies traffic spikes, and helps capacity planning.
Response time (per endpoint) Pinpoints slow operations and bottlenecks. Use P50, P95, and P99 percentiles, not averages.
Service health checks Confirms the service is alive, responsive, and connected to its critical dependencies.
Memory utilization Detects memory leaks before they cause out-of-memory crashes.
CPU utilization Identifies compute-heavy operations and informs scaling and cost-optimization decisions.
Request/response payload size Catches unbounded responses (e.g., endpoints returning entire database collections).
Active request count Confirms concurrency pressure and correlates with response time degradation.

Time-Series Databases

Store your metrics in a purpose-built time-series database. Common choices include InfluxDB (push-based), Prometheus (pull-based), Graphite (push-based), Amazon CloudWatch, and Datadog. Each differs in query language, aggregation model, and cost structure, so evaluate against your specific latency, retention, and budget requirements.

Visualization: Grafana is the most widely adopted open-source metrics dashboard. It integrates natively with all major time-series backends and supports configurable alerts with notification channels for Slack, PagerDuty, email, and webhooks.

3.2 Centralized Logging

The minimum requirement is that every exception and error is logged to a centralized system. SSH-ing into individual servers to grep log files during an incident is a recipe for slow resolution. A centralized logging stack lets you search, filter, and correlate logs across all service instances from a single interface.

Solution Characteristics
ELK Stack (Elasticsearch, Logstash, Kibana) Open-source, highly customizable, self-hosted or managed
Splunk Enterprise-grade, powerful search, higher cost
Loggly / Datadog Logs SaaS, fast setup, pay-per-ingestion

What to Log

  • All errors and exceptions with full stack traces and correlation IDs.
  • High-value business operations (e.g., money transfers, status changes) and the event that triggered them.
  • Authentication and authorization events for audit trails.

What to Guard Against

  • Sensitive data exposure: Never log passwords, API keys, personal identifiers, or financial data in plaintext. Mask or hash sensitive fields before they reach the logging pipeline.
  • Log volume: Excessive logging can saturate storage and increase costs. Use log levels (DEBUG, INFO, WARN, ERROR) and keep production at INFO or above.
  • Access control: Restrict who can read logs. A malicious or negligent insider with log access can extract customer data.

3.3 Alerting and Incident Notification

Metrics and logs only create value when someone acts on them. Configure threshold-based alerts that trigger immediate notification when a critical metric breaches its boundary.

Channel selection: Use the communication tool your team already monitors constantly. If your team lives in Slack, route alerts to a dedicated Slack channel. PagerDuty is the standard for on-call rotation and escalation. Other options include email, Microsoft Teams, Telegram, and custom webhooks.

💡 The best monitoring system in the world is useless if nobody looks at it. Optimize for visibility, not sophistication.


4. Resilience Patterns

Transient faults are inevitable in distributed systems. Networks are unreliable, dependencies fail, and hardware degrades. Resilient services anticipate these failures and handle them gracefully rather than propagating them to the end user.

4.1 Timeout Policies

Every outbound call (database query, HTTP request, message publish) must have an explicit timeout. Without one, a single unresponsive dependency can exhaust your connection pool and cascade failure across the entire service. Set timeouts based on observed P99 response times with a reasonable buffer.

4.2 Retry Policies with Backoff

Many faults are transient and self-correct within milliseconds. A well-configured retry policy recovers from these without user impact. Always combine retries with exponential backoff and jitter to avoid thundering-herd scenarios where all retries fire simultaneously.

  • Limit the maximum number of retries (typically 2–3 for synchronous calls).
  • Only retry on transient error codes (e.g., 503, 429, network timeouts). Never retry on 400-level client errors.
  • Add randomized jitter to backoff intervals to decorrelate retry storms.

4.3 Circuit Breakers

When a downstream dependency is failing consistently, continuing to send it traffic only makes recovery harder. A circuit breaker monitors failure rates and, once a threshold is exceeded, short-circuits requests immediately, returning a fast failure instead of waiting for a timeout. After a cool-down period, it allows a limited number of probe requests to test whether the dependency has recovered.

State Behavior
Closed All requests pass through normally. Failure count is tracked.
Open All requests fail immediately without contacting the dependency.
Half-Open A limited number of test requests are allowed through to check recovery.

4.4 Fallback Strategies

Even with retries and circuit breakers, some requests will fail. Plan what happens next. Common fallback strategies include returning cached stale data, serving a degraded response with partial information, queuing the request for later processing, or returning a user-friendly error with clear retry guidance.

4.5 Bulkhead Isolation

Isolate critical resources so that a failure in one area does not consume resources needed by another. For example, use separate thread pools or connection pools for different downstream dependencies. If the inventory service becomes slow, it should not starve the connection pool that the payment service relies on.


5. Caching Strategies

Network calls are roughly seven orders of magnitude slower than in-memory lookups. Effective caching reduces latency, lowers database load, and improves throughput — but it introduces complexity around data freshness that must be managed deliberately.

5.1 When to Cache (and When Not To)

Cache data that is read frequently, changes infrequently, and can tolerate brief staleness. Displaying a video's view count as 500 instead of 513 is acceptable; displaying an incorrect bank account balance is not.

5.2 In-Memory vs. Distributed Cache

Dimension In-Memory Cache Distributed Cache
Speed Fastest possible (nanosecond access) Fast (sub-millisecond to low milliseconds)
Scalability Limited to a single instance Scales horizontally across the cluster
Data sharing Not shared between instances Shared across services and instances
Consistency Difficult to synchronize across replicas Single source of truth (with caveats)
Failure mode Lost on restart Survives individual node restarts
Examples Guava, Caffeine, node-cache Redis, Memcached, Aerospike, DynamoDB

5.3 Cache Invalidation Strategies

  1. In-place updates: Increment or decrement counters directly on write operations rather than recomputing from the source.
  2. Database change streams: Listen to the operational log (oplog, WAL, CDC) of your database. This approach is robust because no write can be missed, even those triggered by database-internal logic like triggers.
  3. Periodic refresh: Schedule a background job that queries the database and updates the cache at fixed intervals. Trades consistency for reduced cache pressure.
  4. Event-driven invalidation: Publish change events to a message bus and have a dedicated consumer update the cache. Decouples business logic from cache management.

💡 Between persisting data and publishing the change event, there is always a window where a crash or network failure can cause the cache to miss an update. Supplement event-driven invalidation with a periodic full-refresh as a safety net.

5.4 Cache Design Checklist

  • What data should be cached? Identify the hottest read paths.
  • What expiration policy fits? TTL, LRU, or explicit invalidation?
  • Where should the cache live? In-process, sidecar, or external cluster?
  • How many consumers will share it? This affects topology and capacity.
  • What is the expected load? Size the cache to handle peak, not average.

6. Traffic Management

Uncontrolled traffic is one of the most common causes of service degradation. A well-designed traffic management layer protects your services from both organic spikes and abusive traffic patterns.

6.1 Rate Limiting and Throttling

Enforce per-client or per-endpoint rate limits to prevent any single consumer from overwhelming the system. Return HTTP 429 Too Many Requests with a Retry-After header so well-behaved clients can self-regulate.

6.2 Auto-Scaling and Load Balancing

Configure auto-scaling policies based on CPU utilization, request count, or queue depth. Combine with health-check-aware load balancers that automatically remove unhealthy instances from rotation. Use scheduled scaling when you can predict traffic patterns (e.g., end-of-month billing runs).

6.3 Request Buffering

For workloads that tolerate asynchronous processing, use a message queue (RabbitMQ, Kafka, Amazon SQS, Amazon Kinesis) to buffer incoming requests. This decouples ingestion rate from processing rate, naturally smoothing traffic spikes and preventing service overload. This pattern works especially well in event-driven architectures where the UI does not require a synchronous response.


7. Security

Security is not a feature you add later; it is a property of the system you build from the start.

7.1 Transport Security

All internet-facing services must enforce HTTPS. Never transmit sensitive data over unencrypted HTTP. Remember that every consumer has at least one man-in-the-middle: their Internet Service Provider. Internal service-to-service traffic should also use TLS, particularly in multi-tenant or shared-infrastructure environments.

7.2 Access Control

Apply the principle of least privilege at every layer. Each service should have only the minimum permissions it needs to perform its function. AWS IAM is a strong reference model: fine-grained policies that restrict which services can call which endpoints. If the Taxation service has no reason to access the Reporting service, the network and identity layer should enforce that boundary.

7.3 Additional Security Practices

  • Input validation: Validate and sanitize all inbound data at the API boundary. Reject malformed input early.
  • Secret management: Never hardcode credentials. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) and rotate keys on a regular schedule.
  • Dependency scanning: Continuously scan your dependency tree for known vulnerabilities. Automate patching where possible.
  • Audit logging: Record who accessed what and when. This is essential for compliance, forensics, and incident response.

8. Testing for Resilience

Resilience is only theoretical until you test it under adversarial conditions. The goal is to verify that your system degrades gracefully rather than catastrophically when things go wrong.

8.1 Chaos Engineering

Deliberately inject failures into production or staging environments to discover weaknesses before your users do. Common experiments include terminating random instances, introducing network latency between services, simulating dependency outages, and filling disk volumes. Tools like Chaos Monkey (Netflix), Litmus (CNCF), and AWS Fault Injection Simulator provide structured frameworks for running these experiments safely.

8.2 Load and Stress Testing

Validate that your auto-scaling, rate limiting, and circuit breakers behave correctly under sustained high load. Use tools such as k6, Locust, or Gatling to simulate realistic traffic patterns, including sudden spikes and slow ramps. Measure not just throughput but error rates and latency percentiles under pressure.

8.3 Contract Testing

Verify that changes to a service's API do not break its consumers. Consumer-driven contract tests (using Pact or similar frameworks) formalize the expectations each consumer has of a provider. These tests catch breaking changes in CI before they reach production.

8.4 Disaster Recovery Drills

Periodically rehearse your disaster recovery procedures. Failover to a secondary region, restore from backups, and rotate compromised credentials under realistic time pressure. A recovery plan that has never been tested is an assumption, not a plan.


9. Deployment and Release Strategy

How you release code is as important as how you write it. A bad deployment process turns every release into a high-risk event.

9.1 Progressive Rollouts

Avoid deploying changes to 100% of traffic at once. Use canary deployments (route a small percentage of traffic to the new version and monitor for anomalies), blue-green deployments (maintain two identical environments and switch traffic atomically), or rolling deployments (replace instances gradually). Each approach trades off between rollback speed, infrastructure cost, and operational complexity.

9.2 Feature Flags

Decouple deployment from activation. Feature flags let you ship code to production in a dormant state and enable it for specific users, regions, or percentages of traffic without redeploying. This enables safe experimentation, instant rollback of problematic features, and gradual rollout to larger audiences.

9.3 Immutable Infrastructure

Treat servers as disposable. Build new machine images or container images for every deployment rather than modifying running instances in place. Immutable infrastructure eliminates configuration drift, makes rollbacks trivial (redeploy the previous image), and ensures that every environment is reproducible from source.


10. Documentation and Operational Runbooks

A system is only as resilient as the team operating it. Documentation bridges the gap between the engineers who built the system and the engineers who are on call at 3 AM.

10.1 Service Catalog

Maintain a central registry of every service: what it does, who owns it, what it depends on, and how to reach the owning team. This is the first place anyone should look during an incident.

10.2 Runbooks

For every alert, write a corresponding runbook that explains what the alert means, what to check first, likely root causes, and step-by-step remediation actions. Runbooks reduce mean time to resolution (MTTR) and enable less experienced on-call engineers to handle incidents effectively.

10.3 Architecture Decision Records (ADRs)

Document significant technical decisions and the reasoning behind them. When a future engineer asks why the caching layer uses Redis instead of Memcached, the ADR provides the context. This prevents repeated debates and accidental reversals of deliberate trade-offs.

10.4 Post-Incident Reviews

After every significant incident, conduct a blameless post-mortem. Document what happened, what the impact was, what the timeline looked like, what the root cause was, and what concrete actions will prevent recurrence. Share the write-up broadly. Incidents are the highest-signal learning opportunities your organization has.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment