Anti-patterns in Software Architecture
Common mistakes in distributed software architecture: how to spot them, their real impact, and the strategies to fix them.
What anti-patterns are
An anti-pattern is a recurring solution that seems reasonable at first but causes significant problems in the medium and long term. Unlike a simple mistake, anti-patterns are common traps that experienced teams fall into, often due to time pressure, lack of context, or decisions that were correct at one point but stopped being so.
Recognizing these anti-patterns early lets you avoid costly technical debt and make more informed architectural decisions.
Why anti-patterns arise
Anti-patterns donβt appear out of incompetence. They arise for understandable reasons:
| Cause | Example |
|---|---|
| Time pressure | βLetβs add this feature to the existing service, thereβs no time to create a new oneβ |
| Lack of context | βAll services can read from this database, itβs easier that wayβ |
| Decisions that age | βWhen we were 3 developers this worked fine, now weβre 30β |
| Premature optimization | βLetβs split everything into microservices from day 1β |
| Organizational inertia | βWeβve always done it this wayβ |
1. Distributed Monolith
Problem
This happens when a system is split into multiple services that depend on each other so tightly that they cannot be deployed, scaled, or evolved independently. In practice, you get the operational complexity of microservices without any of their benefits.
ββββββββββββ ββββββββββββ ββββββββββββ
βService A ββββββΊβService B ββββββΊβService C β
β βββββββ βββββββ β
ββββββββββββ ββββββββββββ ββββββββββββ
β β β
ββββββββββββββββββ΄βββββββββββββββββ
Mandatory joint deployment
Shared data models
Chained synchronous calls
The clearest signs are:
- You need to deploy several services at once for a change to work
- A change to one serviceβs contract breaks the others
- Services share internal libraries containing business logic
- Integration tests require spinning up the entire system
Impact
- Fragile deployments: A change in one service requires coordinating the deployment of others, eliminating the agility that motivated the split
- Cascading failures: The synchronous dependency between services means one going down drags the others with it
- Complexity without benefit: The team pays the operational cost of multiple services without gaining real independence
- Reduced development velocity: Teams canβt work in parallel because changes block one another
How to detect it
- Ask: can I deploy this service without touching any other? If the answer is frequently βno,β you have a distributed monolith
- Check whether services share database schemas or libraries containing domain logic
- Observe whether releases require coordination across multiple teams for a single functional change
- Measure the coupling score: count how many services get deployed together in each release
Fix
| Strategy | Description |
|---|---|
| Define clear contracts | Use versioned APIs with explicit contracts. Each service exposes a stable interface and evolves internally without breaking consumers |
| Asynchronous communication | Replace chained synchronous calls with events. Publish an event instead of making a direct call |
| Bounded Contexts (DDD) | Apply DDD to identify the real boundaries of the domain. Each service should own its context |
| Evaluate re-consolidation | If services are so coupled that they canβt be separated, consider merging them back. A well-designed monolith is better than a distributed monolith |
| Anti-Corruption Layer | Use an ACL between services to translate models and reduce direct coupling |
2. Shared Database
Problem
Multiple services read and write directly to the same database, accessing the same tables. This creates invisible coupling: services donβt depend on explicit APIs but on the internal structure of the tables.
ββββββββββββ ββββββββββββ ββββββββββββ
βService A β βService B β βService C β
ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Shared Database β
β β
β βββββββββββ βββββββββββ ββββββββββ β
β β Table X β β Table Y β β Table Zβ β
β βββββββββββ βββββββββββ ββββββββββ β
β β
β Everyone reads and writes to all β
ββββββββββββββββββββββββββββββββββββββββ
The clearest signs are:
- Several services run direct queries against the same tables
- A schema change requires modifying multiple services
- Thereβs no clear βownerβ for each table or dataset
- Database migrations are high-risk events
Impact
- Hidden coupling: Services appear independent but are tied together by the database structure. An
ALTER TABLEcan break services you didnβt even know were using that table - Limited scalability: You canβt scale the database independently per service
- Dangerous migrations: Any schema change is a risk because you donβt know who depends on what
- Impossible to evolve: Each service stays tied to the technology and structure of the shared database
How to detect it
- Check whether more than one service has connection strings to the same database
- Look for queries that access tables βbelongingβ to another functional domain
- Ask: who owns this table? If the answer is βseveral services,β you have this anti-pattern
- Run a dependency analysis: map which services access which tables
Fix
| Strategy | Description |
|---|---|
| Database per Service | Each service has its own database. Only the owning service can read and write its data |
| Expose data via API | If Service B needs data from Service A, it requests it through Aβs API |
| Events for synchronization | Use domain events so services keep local copies of the data they need |
| Outbox Pattern | Reliably publish events when data changes, so other services stay in sync |
| Incremental migration | Donβt try to split everything at once. Identify the most problematic tables and migrate service by service |
3. God Service
Problem
A single service concentrates too many responsibilities and becomes the central point through which almost all of the systemβs logic flows. It usually starts as a legitimate service that keeps accumulating features because βitβs easier to add it here.β
βββββββββββββββββββββββ
β God Service β
β β
β β’ Authentication β
β β’ Users β
β β’ Orders β
β β’ Payments β
β β’ Notifications β
β β’ Reports β
β β’ Inventory β
ββββββββββββ¬βββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
βService X β βService Y β βService Z β
β(simple) β β(simple) β β(simple) β
ββββββββββββ ββββββββββββ ββββββββββββ
Everyone depends on the God Service
The clearest signs are:
- One service has significantly more endpoints than the others
- Most functional changes require modifying that service
- The service has multiple unrelated responsibilities
- The team maintaining it is a constant bottleneck
Impact
- Development bottleneck: Every team needs to modify the same service
- Single point of failure: If the God Service goes down, a large part of the system stops working
- Uneven scalability: You canβt scale payments alone if itβs bundled with reports
- Growing complexity: The service becomes hard to understand, test, and maintain
- Risky deployments: Every deployment touches many features
How to detect it
- Measure lines of code or endpoints per service. If one has 10x more than average, itβs suspicious
- Review the commits: if one service concentrates most changes from multiple teams, itβs probably a God Service
- Ask: can I describe this serviceβs responsibility in a single sentence? If you need a long list, it has too many responsibilities
- Analyze the dependencies: if most services depend on a single one, itβs a God Service
Fix
| Strategy | Description |
|---|---|
| Identify bounded contexts (DDD) | Map the real domains within the God Service. Each domain is a candidate to become an independent service |
| Strangler Fig Pattern | Extract features one by one, creating new services that intercept calls to the God Service |
| Single Responsibility Principle | Each service should have a clear reason to exist |
| Prioritize by impact | Start by extracting the feature that changes most often or that has different scalability requirements |
| API Gateway / BFF | Use a gateway to route requests to specialized services instead of the God Service |
4. Chatty Services
Problem
Services communicate with each other using too many fine-grained calls to complete a single operation. Instead of one request that fetches all the needed information, a service makes multiple small calls.
Service A needs to display a user's profile:
Service A β Users Service: GET /users/123
Service A β Users Service: GET /users/123/preferences
Service A β Orders Service: GET /orders?user=123
Service A β Orders Service: GET /orders/456/details
Service A β Orders Service: GET /orders/789/details
Service A β Payments Service: GET /payments?user=123
Service A β Reviews Service: GET /reviews?user=123
Total: 7 network calls for a single screen
The clearest signs are:
- A business operation generates more than 3-4 calls between services
- Latency is high even though each individual service responds fast
- Logs show bursts of correlated calls for a single request
Impact
- Accumulated latency: 7 calls of 50ms each add up to 350ms in the best case (sequential)
- Fragility: With 7 calls and 99% availability per service, effective availability drops to ~93%
- Excessive network load: Internal traffic can exceed external traffic
- Debugging difficulty: Tracing an error across multiple calls requires distributed tracing
How to detect it
- Count the inter-service calls generated by a single user request
- Measure end-to-end latency vs. the individual latency of each service
- Check whether services expose very granular endpoints
Fix
| Strategy | Description |
|---|---|
| BFF (Backend for Frontend) | Create an intermediate service that aggregates data from multiple services into a single response |
| Aggregated endpoints | Design endpoints that return all the information needed for a use case |
| Local cache | Keep local copies of data that changes rarely |
| Parallel calls | If the calls are independent, execute them in parallel |
| Events for derived data | Use events so each service maintains a materialized view of the data it needs |
5. Anemic Domain Model
Problem
Domain entities are data containers without behavior β they only have getters and setters. All the business logic lives in external services that manipulate these passive entities.
// β Anemic model
class Order {
id: string;
status: string;
lines: OrderLine[];
total: number;
// Only getters and setters, no logic
}
class OrderService {
confirm(order: Order) {
if (order.status !== 'draft') throw new Error('...');
if (order.lines.length === 0) throw new Error('...');
order.status = 'confirmed';
order.total = this.calculateTotal(order.lines);
// All the logic is here, not in Order
}
}
// β
Rich model
class Order {
confirm() {
if (this.status !== 'draft')
throw new Error('Only a draft order can be confirmed');
if (this.lines.length === 0)
throw new Error('The order must have at least one line');
this.status = 'confirmed';
this.total = this.calculateTotal();
this.recordEvent(new OrderConfirmed(this.id));
}
}
Impact
- Scattered logic: Business rules get duplicated across multiple services
- Hard to test: Tests need to set up external services to validate simple rules
- Encapsulation violation: Anyone can modify the entityβs state without going through the rules
- Hard to discover: Itβs not obvious which operations are valid on an entity
Fix
| Strategy | Description |
|---|---|
| Move logic into the entities | Business rules that operate on an entityβs data should live in the entity |
| Apply DDD | Use aggregates with invariants that protect consistency |
| Value Objects | Encapsulate concepts like Money, Email, Address with their own validation |
| Domain Events | Entities publish events when they change state |
6. Database Integration
Problem
Services integrate by reading and writing to other servicesβ tables instead of communicating through APIs or events. Itβs a more subtle variant of the shared database.
Service A needs to know whether a user is premium:
β SELECT type FROM users WHERE id = 123
(accesses Service B's table directly)
β
GET /api/users/123/subscription
(queries Service B through its API)
Impact
- Invisible coupling: Thereβs no explicit contract, only implicit knowledge of the table structure
- Impossible to version: You canβt evolve the schema without breaking the hidden consumers
- No access control: Any service can read or write any data
Fix
| Strategy | Description |
|---|---|
| Explicit APIs | Each service exposes an API to access its data |
| Domain events | Publish changes as events so other services can subscribe |
| ACL Pattern | Create a translation layer between the internal model and the consumers |
Comparative summary
| Anti-pattern | Main signal | Biggest risk | First action |
|---|---|---|---|
| Distributed Monolith | You canβt deploy one service alone | Complexity without benefit | Define contracts and bounded contexts |
| Shared Database | Several services access the same tables | Dangerous migrations | Assign table ownership |
| God Service | One service concentrates all the logic | Single point of failure | Identify domains to extract |
| Chatty Services | Many calls for one operation | Latency and fragility | Create aggregated endpoints or a BFF |
| Anemic Model | Entities without behavior | Scattered, duplicated logic | Move logic into the entities (DDD) |
| Database Integration | Direct queries against other servicesβ tables | Invisible coupling | Expose explicit APIs |
How to prevent anti-patterns
Anti-patterns rarely appear all at once. They accumulate gradually. Some practices that help prevent them:
- Periodic architecture reviews: Regularly assess whether services maintain their independence
- Fitness functions: Define automated metrics that detect coupling (number of dependencies, service size, frequency of joint deployments)
- ADRs (Architecture Decision Records): Document architectural decisions and their rationale
- Clear ownership: Every service, table, and API must have a responsible team
- Razor principle: If you canβt justify why a service is independent, it probably shouldnβt be
- Coupling monitoring: Measure how many services deploy together, how many synchronous calls exist between services, and how many tables are shared
Relationship with other patterns
Anti-patterns are prevented and corrected by applying the right patterns:
Anti-pattern Pattern that fixes it
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
Distributed Monolith β DDD (Bounded Contexts)
β Event-Driven Architecture
β ACL Pattern
Shared Database β Database per Service
β Outbox Pattern (synchronization)
β Event-Driven Architecture
God Service β Microservices Pattern
β DDD (decomposition by domain)
β API Gateway / BFF
Chatty Services β BFF Pattern
β API Gateway Pattern
β Event-Driven Architecture
Anemic Model β DDD (tactical design)
β Aggregates + Domain Events
Database Integration β ACL Pattern
β Outbox Pattern
β Shared Kernel (controlled)
Next steps
Now that you know the most common anti-patterns, dig deeper into the patterns that prevent them: DDD to define clear bounded contexts, Microservices Pattern to decompose correctly, Event-Driven Architecture to decouple services, and BFF Pattern to solve the chatty services problem.