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:

CauseExample
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

StrategyDescription
Define clear contractsUse versioned APIs with explicit contracts. Each service exposes a stable interface and evolves internally without breaking consumers
Asynchronous communicationReplace 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-consolidationIf 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 LayerUse 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 TABLE can 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

StrategyDescription
Database per ServiceEach service has its own database. Only the owning service can read and write its data
Expose data via APIIf Service B needs data from Service A, it requests it through A’s API
Events for synchronizationUse domain events so services keep local copies of the data they need
Outbox PatternReliably publish events when data changes, so other services stay in sync
Incremental migrationDon’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

StrategyDescription
Identify bounded contexts (DDD)Map the real domains within the God Service. Each domain is a candidate to become an independent service
Strangler Fig PatternExtract features one by one, creating new services that intercept calls to the God Service
Single Responsibility PrincipleEach service should have a clear reason to exist
Prioritize by impactStart by extracting the feature that changes most often or that has different scalability requirements
API Gateway / BFFUse 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

StrategyDescription
BFF (Backend for Frontend)Create an intermediate service that aggregates data from multiple services into a single response
Aggregated endpointsDesign endpoints that return all the information needed for a use case
Local cacheKeep local copies of data that changes rarely
Parallel callsIf the calls are independent, execute them in parallel
Events for derived dataUse 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

StrategyDescription
Move logic into the entitiesBusiness rules that operate on an entity’s data should live in the entity
Apply DDDUse aggregates with invariants that protect consistency
Value ObjectsEncapsulate concepts like Money, Email, Address with their own validation
Domain EventsEntities 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

StrategyDescription
Explicit APIsEach service exposes an API to access its data
Domain eventsPublish changes as events so other services can subscribe
ACL PatternCreate a translation layer between the internal model and the consumers

Comparative summary

Anti-patternMain signalBiggest riskFirst action
Distributed MonolithYou can’t deploy one service aloneComplexity without benefitDefine contracts and bounded contexts
Shared DatabaseSeveral services access the same tablesDangerous migrationsAssign table ownership
God ServiceOne service concentrates all the logicSingle point of failureIdentify domains to extract
Chatty ServicesMany calls for one operationLatency and fragilityCreate aggregated endpoints or a BFF
Anemic ModelEntities without behaviorScattered, duplicated logicMove logic into the entities (DDD)
Database IntegrationDirect queries against other services’ tablesInvisible couplingExpose 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.