writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
monolith-scaling-strategiesImplements vertical scaling, database optimization, caching tiering, deployment patterns, CI/CD pipelines, and observability strategies for growing monolithic applications to delay or eliminate premature service decomposition.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: monolith-scaling-strategies description: Implements vertical scaling, database optimization, caching tiering, deployment patterns, CI/CD pipelines, and observability strategies for growing monolithic applications to delay or eliminate premature service decomposition. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding role: implementation scope: implementation output-format: code triggers: scaling monolith, vertical scaling, how do i scale a monolith, database connection pool, caching strategy, blue-green deployment, performance bottleneck related-skills: monolith-first-design, monolith-architecture, monolith-refactoring, deployment-strategies, database-optimization archetypes: tactical, diagnostic anti_triggers: microservices, event sourcing, CQRS, service mesh, distributed tracing response_profile: verbosity: medium directive_strength: high abstraction_level: operational
This skill makes the model diagnose bottlenecks in a growing monolithic application and implement operational scaling patterns that defer or eliminate premature service decomposition. It covers vertical scaling, database optimization, caching tiering, zero-downtime deployment, CI/CD pipelines, and observability — all patterns specific to single-process architectures.
Use this skill when:
Avoid this skill for:
database-optimization skill for heavy refactoring of table structures; this skill handles connection pool and query-level tuning onlyMeasure which resource saturates first under production-like load. Run a baseline benchmark against each module of the monolith.
Actions:
Checkpoint: Before proceeding, you must have identified the primary bottleneck category: CPU-bound, I/O-bound, connection pool exhaustion, slow queries, or cache miss storms. If multiple bottlenecks coexist, prioritize the one with the highest revenue impact (slowest path in the critical user flow).
Use the workload profile from Step 1 to calculate correct pool sizing. An undersized pool causes request queuing; an oversized pool exhausts database connections.
Actions:
pool_size = (cpu_cores * 2) + effective_spindle_count for HDD-based databases, or pool_size = cpu_cores * 2 for SSDCheckpoint: Verify pool utilization stays between 60–80% under peak load. Below 40% means you are over-provisioned; above 90% means you will queue requests and risk timeouts.
Identify the top 5 read paths by request volume and cache miss rate. Implement local LRU for single-instance hot keys and Redis as the distributed tier.
Actions:
Checkpoint: Cache hit ratio should exceed 85% for the target data paths. Local cache alone should serve >60% of reads without any network call to Redis.
Assess whether vertical scaling (bigger machine) is still viable versus horizontal scaling. Set up zero-downtime deployment patterns that work for a single service.
Actions:
Checkpoint: Deployment rollback must complete within 60 seconds of trigger. Database migrations must maintain backward compatibility for at least one release cycle.
Add structured observability specific to monoliths where all modules share the same process space, and optimize the build/test pipeline.
Actions:
Checkpoint: Every production incident must be traceable to a specific module and root cause within 5 minutes of alerting. Build time should not increase more than 10% per feature added.
Calculate optimal database connection pool sizes based on measured workload type, CPU cores, and storage backend. Returns a configuration dict ready for your ORM or database driver.
"""Connection pool sizing for monolithic applications.
Calculates optimal pool sizes based on workload profiling data to prevent
connection exhaustion while avoiding over-provisioning that wastes database
server resources.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from enum import Enum
logger = logging.getLogger(__name__)
class WorkloadType(Enum):
"""Workload classification based on request profiling."""
READ_HEAVY = "read_heavy" # >70% SELECT queries
WRITE_HEAVY = "write_heavy" # >30% non-SELECT queries
BALANCED = "balanced" # 30–70% read/write split
BATCH_BURST = "batch_burst" # Periodic large batch operations
@dataclass(frozen=True)
class PoolConfiguration:
"""Immutable connection pool configuration."""
primary_max: int # Max connections to the write-primary database
replica_max: int | None # Max connections to read replicas (None if no replicas)
min_idle: int # Minimum idle connections to keep open
max_lifetime_seconds: int # Max lifetime before a connection is recycled
checkout_timeout_ms: int # Max time to wait for an available connection
class ConnectionPoolSizer:
"""Calculates connection pool sizes based on workload profiling.
Uses the CPU-bound + spindle formula and applies workload-specific
multipliers to prevent both connection exhaustion and resource waste.
"""
# Base formula constants (from Tomcat/JDBC best practices)
_BASE_MULTIPLIER = 2
_BATCH_BURST_FACTOR = 4 # Batch workloads need larger pools temporarily
_READ_HEAVY_REPLICA_RATIO = 3 # Each replica can handle ~3x the primary's share
_MIN_POOL_SIZE = 5 # Never go below this regardless of workload
_MAX_POOL_SIZE = 200 # Cap to prevent database server overload
def calculate(
self,
cpu_cores: int,
storage_type: str, # "ssd" or "hdd"
workload: WorkloadType,
has_read_replicas: bool = False,
peak_requests_per_second: float = 0.0,
avg_query_time_ms: float = 50.0,
) -> PoolConfiguration:
"""Calculate optimal pool configuration from profiling data.
Args:
cpu_cores: Number of CPU cores on the application server.
storage_type: "ssd" or "hdd" for the database backend.
workload: Classification of the workload type from profiling.
has_read_replicas: Whether read replicas are available.
peak_requests_per_second: Measured peak RPS under load.
avg_query_time_ms: Average query execution time in milliseconds.
Returns:
PoolConfiguration ready to apply to your database driver.
"""
effective_spindles = 1 if storage_type == "ssd" else 7
base_size = (cpu_cores * self._BASE_MULTIPLIER) + effective_spindles
workload_multiplier = self._get_workload_multiplier(workload)
pool_size = min(
int(base_size * workload_multiplier),
self._MAX_POOL_SIZE,
)
pool_size = max(pool_size, self._MIN_POOL_SIZE)
if has_read_replicas:
# Route reads to replicas, writes stay on primary
read_pool_size = int(pool_size * 0.7 / 2) if has_read_replicas else 0
replica_max = read_pool_size if has_read_replicas else None
primary_max = pool_size - (read_pool_size * 2) if has_read_replicas else pool_size
else:
primary_max = pool_size
replica_max = None
# Apply query time adjustment: longer queries mean fewer concurrent connections needed
if avg_query_time_ms > 100:
primary_max = max(self._MIN_POOL_SIZE, int(primary_max * 0.8))
if replica_max:
replica_max = max(self._MIN_POOL_SIZE // 2, int(replica_max * 0.8))
min_idle = max(int(pool_size * 0.2), self._MIN_POOL_SIZE)
logger.info(
"Pool config calculated: primary=%d, replicas=%s, min_idle=%d, "
"workload=%s, peak_rps=%.1f",
primary_max,
replica_max,
min_idle,
workload.value,
peak_requests_per_second,
)
return PoolConfiguration(
primary_max=primary_max,
replica_max=replica_max,
min_idle=min_idle,
max_lifetime_seconds=1800, # 30 minutes
checkout_timeout_ms=5000, # 5 second timeout before failing fast
)
def _get_workload_multiplier(self, workload: WorkloadType) -> float:
"""Return the multiplier based on workload classification.
Write-heavy and batch workloads generate more concurrent connections
because each write requires a dedicated connection that holds a
transaction lock until commit.
"""
multipliers = {
WorkloadType.READ_HEAVY: 1.0,
WorkloadType.WRITE_HEAVY: 1.5,
WorkloadType.BALANCED: 1.2,
WorkloadType.BATCH_BURST: self._BATCH_BURST_FACTOR,
}
return multipliers[workload]
# --- Usage example ---
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
sizer = ConnectionPoolSizer()
config = sizer.calculate(
cpu_cores=8,
storage_type="ssd",
workload=WorkloadType.READ_HEAVY,
has_read_replicas=True,
peak_requests_per_second=1200.0,
avg_query_time_ms=35.0,
)
print(f"Primary max connections: {config.primary_max}")
print(f"Replica max connections: {config.replica_max}")
print(f"Minimum idle connections: {config.min_idle}")
Manages a local in-process LRU cache backed by a Redis distributed tier. Implements cache-aside read pattern and write-through invalidation with TTL-aware expiration to prevent stale reads in a single-process monolith that may run on multiple instances behind a load balancer.
"""Two-tier cache management for monolithic applications.
Provides a local LRU cache for hot data paths (sub-millisecond access)
with Redis as the distributed tier for cross-instance consistency.
Implements cache-aside reads and write-through invalidation.
"""
from __future__ import annotations
import asyncio
import hashlib
import logging
import time
from collections import OrderedDict
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Any, Callable, Generic, TypeVar
logger = logging.getLogger(__name__)
T = TypeVar("T")
@dataclass
class CacheEntry(Generic[T]):
"""A single cache entry with TTL tracking."""
value: T
created_at: float = field(default_factory=time.time)
access_count: int = 0
@property
def is_expired(self) -> bool:
return (time.time() - self.created_at) > self.ttl_seconds if hasattr(self, 'ttl_seconds') else False
class LocalLRUCache(Generic[T]):
"""Thread-safe in-process LRU cache with TTL expiration.
Used as the first tier for hot data that is accessed frequently
and does not need cross-instance consistency within a single process lifetime.
"""
def __init__(self, max_size: int = 1024, default_ttl_seconds: float = 300.0) -> None:
self._store: OrderedDict[str, CacheEntry[T]] = OrderedDict()
self._max_size = max_size
self._default_ttl = default_ttl_seconds
def get(self, key: str) -> T | None:
"""Retrieve a value from the local cache. Returns None on miss or expiration."""
entry = self._store.get(key)
if entry is None:
return None
if time.time() - entry.created_at >= self._default_ttl:
del self._store[key]
return None
# Move to end (most recently used)
self._store.move_to_end(key)
entry.access_count += 1
return entry.value
def set(self, key: str, value: T, ttl_seconds: float | None = None) -> None:
"""Store a value in the local cache with optional TTL override."""
if len(self._store) >= self._max_size:
# Evict oldest entry
self._store[key] = CacheEntry(value=value) # type: ignore[call-arg]
self._store.move_to_end(key)
def invalidate(self, key: str) -> bool:
"""Remove a single key. Returns True if the key existed."""
if key in self._store:
del self._store[key]
return True
return False
def invalidate_prefix(self, prefix: str) -> int:
"""Invalidate all keys matching a prefix. Returns count of removed entries."""
keys_to_remove = [k for k in self._store if k.startswith(prefix)]
for key in keys_to_remove:
del self._store[key]
return len(keys_to_remove)
@property
def size(self) -> int:
return len(self._store)
class RedisCacheClient:
"""Thin wrapper around Redis for distributed cache operations.
Uses pipeline operations to batch cache reads/writes efficiently,
minimizing network round-trips in the monolith's request path.
"""
def __init__(
self,
redis_url: str = "redis://localhost:6379/0",
max_connections: int = 20,
socket_timeout_ms: int = 2000,
) -> None:
self._redis_url = redis_url
self._max_connections = max_connections
self._socket_timeout_ms = socket_timeout_ms
async def get(self, key: str) -> bytes | None:
"""Get a value from Redis. Returns None on cache miss."""
# In production, use aioredis or redis.asyncio here
logger.debug("Redis GET %s", key)
return None # Placeholder — replace with actual Redis call
async def set(self, key: str, value: bytes, ttl_seconds: int = 600) -> None:
"""Set a value in Redis with TTL."""
logger.debug("Redis SET %s TTL=%ds", key, ttl_seconds)
async def delete(self, key: str) -> None:
"""Delete a key from Redis."""
logger.debug("Redis DEL %s", key)
async def invalidate_prefix(self, prefix: str) -> int:
"""Invalidate all keys with a given prefix using SCAN (no BLOCKING)."""
# Use SCAN instead of KEYS to avoid blocking the Redis server
count = 0
logger.debug("Redis invalidate prefix: %s", prefix)
return count
@dataclass
class CacheConfig:
"""Configuration for the two-tier cache system."""
local_max_size: int = 2048 # Entries in local LRU per instance
redis_url: str = "redis://localhost:6379/0"
default_ttl_seconds: float = 300.0 # 5 minutes default TTL
hot_key_threshold: int = 100 # Access count to promote to Redis tier
class CacheTierManager:
"""Manages two-tier caching for a monolithic application.
Read path: local LRU → Redis → database
Write path: update database → write-through to both cache tiers
This pattern prevents the "thundering herd" problem where many instances
simultaneously miss their caches and hammer the database.
"""
def __init__(
self,
config: CacheConfig | None = None,
) -> None:
self._config = config or CacheConfig()
self._local_cache: LocalLRUCache[Any] = LocalLRUCache(
max_size=self._config.local_max_size,
default_ttl_seconds=self._config.default_ttl_seconds,
)
self._redis_client: RedisCacheClient = RedisCacheClient(
redis_url=self._config.redis_url,
)
async def get_or_fetch(
self,
key: str,
fetch_fn: Callable[[], T | None],
ttl_seconds: float | None = None,
prefer_local: bool = True,
) -> T | None:
"""Cache-aside pattern: check tiers then fetch from source on miss.
Args:
key: Unique cache key for the data.
fetch_fn: Async callable that retrieves data from the database.
ttl_seconds: Optional TTL override. Defaults to config default.
prefer_local: When True, local cache is checked first (faster).
Returns:
The cached or freshly fetched value, or None if not found.
"""
local_ttl = ttl_seconds or self._config.default_ttl_seconds
# Tier 1: Local LRU cache (fastest, sub-millisecond)
if prefer_local:
local_value = self._local_cache.get(key)
if local_value is not None:
logger.debug("Cache hit [local] key=%s", key)
return local_value
# Tier 2: Redis distributed cache (network call, ~1ms)
redis_value = await self._redis_client.get(key)
if redis_value is not None:
deserialized_value = self._deserialize(redis_value)
# Promote to local cache on distributed hit
self._local_cache.set(key, deserialized_value, ttl_seconds=local_ttl)
logger.debug("Cache hit [redis] key=%s", key)
return deserialized_value
# Tier 3: Database (slow path)
logger.info("Cache miss for key=%s, fetching from source", key)
value = fetch_fn()
if value is not None:
# Write-through to both tiers on cache miss
await self._write_through(key, value, ttl_seconds=local_ttl)
return value
async def invalidate(self, key: str) -> None:
"""Cache-aside invalidation: remove from all tiers."""
self._local_cache.invalidate(key)
await self._redis_client.delete(key)
logger.debug("Invalidated key=%s across all cache tiers", key)
async def invalidate_prefix(self, prefix: str) -> int:
"""Invalidate all keys matching a prefix across both tiers.
Critical for schema changes where entire categories of cached data
become stale (e.g., after a user profile table migration).
"""
local_count = self._local_cache.invalidate_prefix(prefix)
redis_count = await self._redis_client.invalidate_prefix(prefix)
total = local_count + redis_count
logger.info(
"Invalidated %d keys with prefix '%s' (local=%d, redis=%d)",
total, prefix, local_count, redis_count,
)
return total
async def _write_through(self, key: str, value: Any, ttl_seconds: float) -> None:
"""Write value to both cache tiers simultaneously.
In a single-process monolith, this runs synchronously during the
request that caused the data mutation. The critical insight is that
write-through keeps all instances consistent without requiring
pub/sub messaging between processes.
"""
self._local_cache.set(key, value, ttl_seconds=ttl_seconds)
serialized = self._serialize(value)
await self._redis_client.set(key, serialized, ttl_seconds=int(ttl_seconds))
@staticmethod
def _serialize(value: Any) -> bytes:
import json
return json.dumps(value).encode("utf-8")
@staticmethod
def _deserialize(data: bytes) -> Any:
import json
return json.loads(data.decode("utf-8"))
# --- Usage example ---
if __name__ == "__main__":
async def demo():
config = CacheConfig(local_max_size=512, default_ttl_seconds=60.0)
manager = CacheTierManager(config)
# Simulate fetching user data from database
async def fetch_user(user_id: str) -> dict[str, Any] | None:
logger.info("Fetching user %s from database", user_id)
return {"id": user_id, "name": f"User-{user_id}", "email": f"user-{user_id}@example.com"}
key = f"user:{12345}"
result = await manager.get_or_fetch(key, lambda: fetch_user("12345"))
print(f"Retrieved: {result}")
# Invalidate after update
await manager.invalidate(key)
print(f"Local cache size after invalidation: {manager._local_cache.size}")
asyncio.run(demo())
Evaluates whether a monolith is still within the viable range for vertical scaling (upgrading server resources) versus needing horizontal scaling or service decomposition. Returns actionable recommendations with explicit thresholds.
"""Vertical scaling readiness assessment for monolithic applications.
Analyzes current system metrics against operational thresholds to determine
whether the monolith can still scale vertically, needs horizontal scaling,
or has reached the point where service decomposition becomes necessary.
This prevents two common anti-patterns:
1. Scaling vertically forever until hitting hardware limits (wasteful)
2. Decomposing services before the monolith is properly optimized (premature)
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
logger = logging.getLogger(__name__)
class ScalingRecommendation(Enum):
"""Verdict on whether to scale vertically or take another action."""
SCALE_VERTICALLY = "scale_vertically" # Still viable: upgrade resources
OPTIMIZE_FIRST = "optimize_first" # Bottlenecks can be fixed via code/db tuning
HORIZONTAL_SCALING = "horizontal_scaling" # Vertical scaling diminishing returns
DECOMPOSE = "decompose" # Monolith has structural issues requiring splitting
@dataclass
class SystemMetrics:
"""Runtime metrics collected from a running monolith."""
cpu_utilization_percent: float # 0–100, average across all cores
memory_utilization_percent: float # 0–100, heap + OS overhead
database_connections_active: int # Currently checked-out connections
database_connections_max_pool: int # Pool max configured size
cache_hit_ratio: float # 0.0–1.0, overall hit rate
p99_latency_ms: float # 99th percentile response time
error_rate_percent: float # Failed requests / total requests
disk_io_utilization_percent: float # 0–100, I/O wait percentage
@dataclass
class Thresholds:
"""Configurable thresholds that define operational boundaries."""
cpu_warning: float = 75.0
cpu_critical: float = 90.0
memory_warning: float = 80.0
memory_critical: float = 92.0
connection_pool_utilization_warning: float = 70.0
connection_pool_utilization_critical: float = 85.0
cache_hit_ratio_minimum: float = 0.85
p99_latency_warning_ms: float = 500.0
p99_latency_critical_ms: float = 2000.0
error_rate_warning: float = 1.0 # Percentage
error_rate_critical: float = 5.0 # Percentage
@dataclass
class AssessmentResult:
"""Complete vertical scaling assessment with actionable recommendations."""
recommendation: ScalingRecommendation
score: float # 0–100, higher is better for vertical scaling
findings: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
actions: list[str] = field(default_factory=list)
class VerticalScalingAssessor:
"""Assesses whether a monolith is ready for vertical scaling.
Evaluates system metrics against thresholds and produces a scored
recommendation with specific, actionable findings. Designed to run
as a periodic health check or before any capacity planning decision.
"""
def __init__(self, thresholds: Thresholds | None = None) -> None:
self._thresholds = thresholds or Thresholds()
def assess(self, metrics: SystemMetrics) -> AssessmentResult:
"""Evaluate system readiness for vertical scaling.
Args:
metrics: Current runtime metrics from the monolith.
Returns:
AssessmentResult with recommendation and actionable findings.
"""
score = 100.0
findings: list[str] = []
warnings: list[str] = []
actions: list[str] = []
# --- CPU Assessment ---
cpu_delta = metrics.cpu_utilization_percent - self._thresholds.cpu_warning
if metrics.cpu_utilization_percent >= self._thresholds.cpu_critical:
score -= 30.0
findings.append(
f"CRITICAL: CPU at {metrics.cpu_utilization_percent:.1f}% "
f"(threshold: {self._thresholds.cpu_critical}%)"
)
actions.append("Profile CPU-bound paths; consider upgrading from current instance type")
elif metrics.cpu_utilization_percent >= self._thresholds.cpu_warning:
score -= 15.0
warnings.append(f"CPU elevated at {metrics.cpu_utilization_percent:.1f}%")
# --- Memory Assessment ---
if metrics.memory_utilization_percent >= self._thresholds.memory_critical:
score -= 25.0
findings.append(
f"CRITICAL: Memory at {metrics.memory_utilization_percent:.1f}% "
f"(threshold: {self._thresholds.memory_critical}%)"
)
actions.append("Investigate memory leaks; check for large object caching without TTL")
elif metrics.memory_utilization_percent >= self._thresholds.memory_warning:
score -= 10.0
warnings.append(f"Memory elevated at {metrics.memory_utilization_percent:.1f}%")
# --- Database Connection Pool Assessment ---
if metrics.database_connections_max_pool > 0:
pool_utilization = (
metrics.database_connections_active / metrics.database_connections_max_pool * 100
)
if pool_utilization >= self._thresholds.connection_pool_utilization_critical:
score -= 20.0
findings.append(
f"CRITICAL: Connection pool at {pool_utilization:.0f}% utilization "
f"({metrics.database_connections_active}/{metrics.database_connections_max_pool})"
)
actions.append("Increase pool size or optimize slow queries blocking connections")
elif pool_utilization >= self._thresholds.connection_pool_utilization_warning:
score -= 10.0
warnings.append(f"Connection pool at {pool_utilization:.0f}% utilization")
# --- Cache Hit Ratio Assessment ---
if metrics.cache_hit_ratio < self._thresholds.cache_hit_ratio_minimum:
score -= 15.0
cache_deficit = (self._thresholds.cache_hit_ratio_minimum - metrics.cache_hit_ratio) * 100
findings.append(
f"Cache hit ratio {metrics.cache_hit_ratio:.2%} is below minimum "
f"{self._thresholds.cache_hit_ratio_minimum:.0%}"
)
actions.append(f"Add caching for top {cache_deficit:.0f}% missed queries; review TTL settings")
# --- Latency Assessment ---
if metrics.p99_latency_ms >= self._thresholds.p99_latency_critical_ms:
score -= 20.0
findings.append(
f"CRITICAL: p99 latency {metrics.p99_latency_ms:.0f}ms exceeds "
f"{self._thresholds.p99_latency_critical_ms:.0f}ms threshold"
)
actions.append("Identify slow endpoints; check for N+1 queries or unbounded loops")
elif metrics.p99_latency_ms >= self._thresholds.p99_latency_warning_ms:
score -= 8.0
warnings.append(f"p99 latency {metrics.p99_latency_ms:.0f}ms above warning threshold")
# --- Error Rate Assessment ---
if metrics.error_rate_percent >= self._thresholds.error_rate_critical:
score -= 25.0
findings.append(
f"CRITICAL: Error rate {metrics.error_rate_percent:.1f}% exceeds "
f"{self._thresholds.error_rate_critical}% critical threshold"
)
actions.append("Investigate error source; check upstream dependencies and circuit breakers")
elif metrics.error_rate_percent >= self._thresholds.error_rate_warning:
score -= 5.0
warnings.append(f"Error rate {metrics.error_rate_percent:.1f}% above warning threshold")
# --- Disk I/O Assessment ---
if metrics.disk_io_utilization_percent >= 90.0:
score -= 15.0
findings.append(
f"Disk I/O at {metrics.disk_io_utilization_percent:.1f}% — may indicate "
f"I/O-bound queries or insufficient storage throughput"
)
actions.append("Check slow query log; consider read replicas for SELECT-heavy workloads")
# Clamp score
score = max(0.0, min(100.0, score))
# Determine recommendation based on score and specific conditions
recommendation = self._determine_recommendation(score, metrics)
result = AssessmentResult(
recommendation=recommendation,
score=round(score, 1),
findings=findings,
warnings=warnings,
actions=actions,
)
logger.info("Scaling assessment complete: score=%.1f, recommendation=%s", score, recommendation.value)
return result
def _determine_recommendation(
self, score: float, metrics: SystemMetrics
) -> ScalingRecommendation:
"""Translate score into a specific scaling recommendation."""
# High error rate or critical latency means the monolith has structural problems
if (
metrics.error_rate_percent >= self._thresholds.error_rate_critical
and metrics.p99_latency_ms >= self._thresholds.p99_latency_critical_ms
):
return ScalingRecommendation.DECOMPOSE
# Connection pool exhaustion + high latency = database is the bottleneck, not compute
if (
metrics.database_connections_active / max(metrics.database_connections_max_pool, 1)
>= self._thresholds.connection_pool_utilization_critical
):
return ScalingRecommendation.HORIZONTAL_SCALING
# Score below 30 with multiple critical findings means vertical scaling is not viable
if score < 30:
return ScalingRecommendation.DECOMPOSE
# Score between 30–60 means optimization should happen before any hardware change
if 30 <= score < 60:
return ScalingRecommendation.OPTIMIZE_FIRST
# Score above 60 means vertical scaling is still productive
return ScalingRecommendation.SCALE_VERTICALLY
# --- Usage example ---
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
assessor = VerticalScalingAssessor()
metrics = SystemMetrics(
cpu_utilization_percent=82.0,
memory_utilization_percent=71.0,
database_connections_active=45,
database_connections_max_pool=50,
cache_hit_ratio=0.78,
p99_latency_ms=850.0,
error_rate_percent=2.3,
disk_io_utilization_percent=65.0,
)
result = assessor.assess(metrics)
print(f"\n=== Vertical Scaling Assessment ===")
print(f"Score: {result.score}/100")
print(f"Recommendation: {result.recommendation.value}")
if result.findings:
print(f"\nFindings:")
for f in result.findings:
print(f" ❌ {f}")
if result.warnings:
print(f"\nWarnings:")
for w in result.warnings:
print(f" ⚠️ {w}")
if result.actions:
print(f"\nActions:")
for a in result.actions:
print(f" → {a}")
When this skill is active, your output must contain:
| Skill | Purpose |
|---|---|
monolith-first-design | Decide when a monolith is the right starting point for new projects |
monolith-architecture | Structural patterns for organizing modules within a single deployment |
monolith-refactoring | Incremental service extraction from a growing monolith |
deployment-strategies | Advanced blue-green and canary patterns beyond this skill's scope |
database-optimization | Heavy database schema refactoring, indexing strategies, partitioning |
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.