Performance Antipatterns
Performance antipatterns in system design refer to common mistakes or suboptimal practices that can lead to poor performance in a system.
Busy Database
The busy database antpattern occurs when an application shifts too much business logic, processing or non-relational work onto the database engine, turning it into performance bottleneck for the entire system. Instead of treating the database as a reliable storage and retrieval engine, the application treats it as a general-purpose compute cluster. Since scaling a database is way harder than scaling a server this can cripple system scalability.
| Antipattern Behavior | The Solution |
|---|---|
Complex Calculations in Queries: Running heavy math or formatting inside SELECT statements. | Move to App Layer: Fetch the raw data and let the application servers handle the formatting, math, and business rules. |
| Real-time Analytics: Running heavy reporting queries against the live transactional database (OLTP). | Read Replicas or Data Warehouses: Offload reporting to a dedicated read-only database replica or a dedicated data warehouse (OLAP). |
Heavy Text/Search Processing: Relying on LIKE '%text%' queries or intensive database text filtering. | Search Engines: Offload search tasks to dedicated search indexes like Elasticsearch or OpenSearch. |
| Repetitive Data Fetching: Querying the database for static or slowly changing configuration data. | Caching Layer: Implement an in-memory cache (like Redis) in front of the database to intercept repetitive reads. |
Busy Front End
The Busy Frontend Antipattern occurs when a client-side application (browser, mobile app) takes on too much responsibility, transforming from a lean user interface into a heavy processing engine.Instead of focusing purely on rendering UI and handling user interactions, the frontend handles heavy data manipulation, complex business logic, or excessive state management, resulting in sluggish performance, battery drain, and poor user experience.
| Antipattern Behavior | The Solution |
|---|---|
| Client-Side Filtering & Pagination: Fetching all data at once and filtering it in the UI. | Server-Side Querying: Use API query parameters (e.g., ?page=1&limit=20&search=xyz) to let the backend serve only the necessary data subset. |
| Complex Calculations: Running heavy data analysis or math algorithms in JavaScript. | Compute on the Backend: Move computational heavy lifting to scalable application servers or serverless functions. |
| Bloated Bundle Sizes: Shipping massive JavaScript bundles to the browser. | Code Splitting & Lazy Loading: Break down the application so users only download the code required for the specific page they are viewing. |
| Frontend Enforced Security: Relying on UI hiding/showing to restrict user actions. | Server-Side Validation: Always validate permissions, roles, and business rules on the server, treating the frontend purely as a visual representation. |
Chatty I/O
The Chatty I/O Antipattern occurs when an application makes a large number of small, separate network requests to execute a single business transaction, rather than combining them into a few efficient calls. This frequently happens in client-to-server communication or in microservices architectures where systems spend more time dealing with network protocol overhead than actual data processing. It can lead to increase network latency, resource exhaustion and high battery and data usage.
| Antipattern Behavior | The Solution |
|---|---|
| Sequential Small Requests: Making multiple distinct API calls to fill out a single page UI(N+1 Pattern). | Data Bundling / Aggregation: Combine data into a single, comprehensive payload. Use a Gateway Aggregator pattern or GraphQL to allow the client to request all needed resources at once. |
Row-by-Row Database Queries: Running a query inside a foreach loop. | Batch Processing: Use batch operations (e.g., INSERT INTO ... VALUES (...) with multiple rows, or SELECT ... WHERE id IN (...)) to handle all data in a single round-trip. |
| Excessive Internal Microservice Calls: Services constantly pinging each other for tiny pieces of data. | Service Rescoping or Caching: Consolidate overly chatty nanoservices into a single service, or introduce an in-memory caching layer (like Redis) so services don't have to cross the network for every request. |
Extraneous Fetching
The Extraneous Fetching Antipattern occurs when an application retrieves significantly more data from a database or remote service than is actually required to complete a business transaction. It focuses on wasting memory and network bandwidth by moving massive, unnecessary payloads across the network. It can lead to memory bloat, network i/o bottlenecks, inefficient database indexing, etc.
| Antipattern Behavior | The Solution |
|---|---|
Using SELECT * Everywhere: Fetching all columns out of convenience. | Projected Queries: Explicitly name only the columns you need (e.g., SELECT id, name FROM users). |
| In-Memory Filtering: Pulling large datasets to filter them via code. | Database-Side Filtering: Push the filtering logic to the database using WHERE clauses, indexes, and aggregation functions so only the matching rows cross the network. |
| Aggressive ORM Eager Loading: Automatically pulling in deeply nested relationships. | Lazy Loading / Explicit Joins: Switch to lazy loading by default, and explicitly fetch related entities only when they are guaranteed to be used in that specific transaction. |
| Bloated API Endpoints: Getting fields you don't need from a REST endpoint. | Field Filtering or GraphQL: Implement query parameters that allow clients to specify desired fields (e.g., /users?fields=id,name), or migrate to GraphQL to let the client control the payload shape. |
Improper Instantiation
The Improper Instantiation Antipattern occurs when an application manages object lifecycles inefficiently either by repeatedly creating new instances of an object that should be shared (over-instantiation) or by creating resource-heavy objects before they are actually needed. This frequently happens when developers ignore the operational cost of creating specific objects, leading to high memory consumption, CPU spikes, and garbage collection bottlenecks. These can lead to increasing garbage collection pressure, resource exhaustion, high startup and executiono latency.
| Antipattern Behavior | The Solution |
|---|---|
| Re-creating Network/DB Clients: Instantiating a new client for every API call or database query. | Re-use Instances / Pooling: Maintain a single, long-lived instance of network clients (e.g., reusing an HttpClient) or pull connections from a dedicated connection pool. |
| Instantiating Stateless Utilities: Creating objects that hold no state over and over. | Dependency Injection / Singletons: Register the class as a Singleton in your Dependency Injection (DI) container so a single instance is shared globally. |
| Expensive Objects Created on Startup: Loading massive resources that might not be used. | Lazy Initialization: Use lazy loading patterns (e.g., Lazy<T> in C# or initializing on first use) so the resource is only created the exact moment it is needed. |
Manual Thread Creation: Writing new Thread() inside application logic. | Thread Pools: Offload tasks to the system's built-in thread pool or execution framework, allowing the runtime to recycle threads efficiently. |
Monolithic Persistence
The Monolithic Persistence Antipattern occurs when an application architecture forces all of its services, components, or microservices to share a single, central database instance and schema. While common in traditional monolithic systems, this pattern becomes an architectural bottleneck when systems try to scale, transition to microservices, or handle diverse data requirements. It tightly couples independent domains at the data tier, breaking system isolation. This can lead problems such as deployment deadlocks due to tight coupling, single point of failure, sub-optimal data modelling.
| Antipattern Behavior | The Solution |
|---|---|
| Shared Tables Across Services: Multiple services reading/writing to the same tables. | Database-per-Service Pattern: Encapsulate data. Each service owns its private database. Other services can only access that data via authorized APIs. |
Direct SQL Joins Across Domains: Joining Orders and Customers tables across services. | API Composition or CQRS: Have the application layer query both APIs and combine the data, or implement Command Query Responsibility Segregation (CQRS) with data duplication via events. |
| Forcing All Data Types Into SQL: Storing logs, sessions, and catalog data in one relational DB. | Polyglot Persistence: Match the tool to the job. Use Redis for sessions, Elasticsearch for text search, Neo4j for graphs, and PostgreSQL for financial transactions. |
| Direct Analytics on Live Data: Running BI tools against the primary transactional database. | Data Pipelines (ETL): Stream data changes out of individual service databases into a centralized Data Lake or Data Warehouse (like Snowflake or BigQuery) for reporting. |
Noisy Neighbor
It happens in multi-tenant cloud or shared infrastructure environments when one tenant (user, application, or service) monopolizes shared resources, causing a severe drop in performance for other tenants sharing the same physical hardware. In shared environments, resources like CPU, memory, disk I/O, and network bandwidth are pooled. If one application experiences a massive traffic spike or runs an unoptimized, resource-heavy job, it "starves" its neighbors. It can lead to unpredictable performance for other resources, hard to debug latency spikes, voilation of SLA's for uptime and response times due to single rogue user.
| Antipattern Behavior | The Solution |
|---|---|
| Unrestricted Resource Access: Applications taking as much CPU/RAM as they want. | Resource Quotas & Throttling: Enforce strict hard limits on CPU shares, memory allocation, and IOPS using container orchestration tools like Kubernetes or Docker cgroups. |
| Shared Network Pipes: One app saturating the network infrastructure. | Rate Limiting & Traffic Shaping: Implement API Gateways with rate-limiting policies (e.g., maximum requests per second per IP) to block abusive traffic before it hits shared infrastructure. |
| High Impact Monolithic Co-location: High-priority apps mixed with unpredictable apps. | Dedicated Instances / Isolation: Move mission-critical, high-throughput applications off shared public infrastructure onto dedicated cloud instances, bare-metal servers, or isolated single-tenant clusters. |
| Unnoticed Resource Starvation: Neighbors slowing down without clear alerts. | Anomaly Detection Monitoring: Set up infrastructure-level monitoring to instantly alert administrators when a single container or VM deviates sharply from its baseline resource consumption. |
Synchronous/Blocking I/O
It happens when an application initiates an I/O operation such as reading from a file, hitting a database, calling an external API and completely block its execution thread leading to execution thread being idle and wasting compute time which could be used by other application. It can lead to exhaustion of thread pool such as database connection pools, massive overhead for cpu as it has to constantly do context switching for different processes and poor hardware utilization made for parallel execution such as modern nvme's.
| Antipattern Behavior | The Solution |
|---|---|
Using Blocking System Calls: Using synchronous file or network methods (e.g., fs.readFileSync). | Async/Await Syntax: Switch to native asynchronous primitives (async/await, Promises, or Callbacks) so the thread is released back to the system until the data is ready. |
| Sequential, Dependent I/O: Waiting for one independent query to finish before starting the next. | Parallel I/O Execution: Fire off independent I/O requests concurrently using mechanisms like Promise.all() or parallel thread execution, then await the combined results. |
| Heavy Logging to Disk: Writing logs directly to local storage synchronously on the main thread. | Asynchronous Logging / Message Queues: Offload logging operations to a background thread pool or a fast memory buffer that flushes to disk asynchronously. |
Retry Storm
When a backend service experiences a minor slowdown or outage, and dependent client applications respond by aggressively retrying their failed requests all at the same time. Instead of helping, this flood of automated retries acts exactly like a self-inflicted Distributed Denial of Service (DDoS) attack, completely overwhelming the struggling service and preventing it from recovering. It can lead to pool exhaustion due to the insane number of request, normal requests getting timed out, crashing of the server, etc.
| Antipattern Behavior | The Solution |
|---|---|
| Retrying Instantly or at Fixed Intervals: Flooding the network in synchronized waves. | Exponential Backoff: Increase the wait time exponentially between each retry attempt (e.g., wait 1s, then 2s, then 4s, then 8s). |
| Synchronized Client Requests: Clients hitting the server at the exact same millisecond. | Jitter (Randomization): Add random noise to the backoff interval (e.g., instead of exactly 4s, wait 4.2s or 3.8s). This breaks up client synchronization and flattens the traffic spike. |
| Hammering a Dead Service: Continuing to send requests to a backend that is obviously failing. | Circuit Breaker Pattern: Place a "circuit breaker" in front of the API. If failure rates cross a threshold (e.g., 50% failures), the circuit opens, and all subsequent requests fail fast instantly without hitting the backend, giving it time to heal. |
| Infinite Loops: Retrying indefinitely until resources run dry. | Dead-Letter Queues (DLQ) / Max Retries: Strictly cap retry attempts (usually 3 to 5 max). If it still fails, log the error, send it to a DLQ for investigation, and return a clean error message to the user. |
No Caching
It happens when an application repeatedly requests, processes, or generates identical data for every single transaction, completely ignoring opportunities to store and reuse that data in memory. This forces systems to perform expensive operations over and over such as hitting databases, rendering complex views, or calling third-party APIs—for data that changes rarely or not at all. This leads to artificial database bottlenecks due to useless database queries, elevated operational cost, increased latency for other requests.
| Antipattern Behavior | The Solution |
|---|---|
| Frequent DB Reads for Static Data: Querying the database constantly for unchanging records. | In-Memory Caching (Redis/Memcached): Intercept database calls by checking a fast, in-memory key-value store first. If the data is missing (cache miss), fetch it from the DB and save it to the cache. |
| Origin Server Asset Flooding: Servicing requests for images, CSS, and JS directly from your server. | CDN Integration & HTTP Headers: Route traffic through a CDN (like Cloudflare) and append explicit Cache-Control: public, max-age=31536000 headers to your static assets. |
| Expensive Downstream API Calls: Repeatedly executing identical calls to third-party providers. | Time-To-Live (TTL) Caching: Store external API responses locally with a designated TTL (expiration window), reusing the data until the TTL expires. |
| Heavy UI Component Re-rendering: Constantly rebuilding complex data views. | Application-Level Memoization: Cache the computed output of heavy functions or data structures directly in the application server's local memory buffer. |
