Architectural Anti-Patterns
The most common ways to do architecture wrong — distributed monolith, microservice-per-table, premature event sourcing, and the others that recur in real codebases
Architectural Anti-Patterns
Every architectural pattern has a failure mode. Most of those failure modes have been observed enough times that the industry has names for them. This page is a catalogue: what each anti-pattern looks like, why teams adopt it (usually unintentionally), what damage it causes, and how to escape.
The list is not exhaustive — anti-patterns are emergent and new ones appear with new tooling — but these are the patterns you are most likely to encounter, cause, or have to clean up.
Distributed Monolith
Symptoms. Multiple services that must deploy together in a specific order. A feature change touches three services every time. Releases require a coordinator role to sequence them. Service A reaches into service B's database. Compile-time dependencies in shared libraries force coordinated upgrades.
Why it happens. Teams adopt microservices without the discipline (or capacity) for true independence. The split happens too early, before bounded contexts are clear, or along data tables instead of along bounded contexts. Shared databases persist "for now." Shared schemas in shared libraries proliferate.
Damage. You pay the full distribution tax (network failure, latency, idempotency, operational overhead) and get none of the autonomy benefits. Independent deploys are impossible. Independent scaling barely works because the services are functionally coupled. The system is worse than a regular monolith because it is also a distributed system.
Escape. Either recombine into a real monolith (or modular monolith) or invest in genuine independence: separate databases per service, versioned APIs with deprecation windows, the outbox pattern for cross-service events, and team boundaries that match service boundaries. Half-measures stay broken.
Microservice-per-Table
Symptoms. A users-service, an orders-service, an order-items-service, a products-service. Each owns one table. Every business operation involves three or four services calling each other. Each service has an anemic API (GetUser, UpdateUser) — essentially CRUD over the network.
Why it happens. "Microservices means small services." Database tables are the easiest visible unit to split. Teams skip the bounded context work because it is conceptually harder than picking tables.
Damage. Maximum distribution tax for minimum autonomy benefit. Cross-service joins replace database joins, with worse performance and reliability. Domain logic spreads across services or, more often, accumulates in BFFs and clients. The "services" do not encapsulate behavior, just data access.
Escape. Merge related services back into bounded-context-aligned services. A customer-service that owns user, profile, preferences, and order history is more honest than four CRUD services. This requires acknowledging the split was wrong — politically harder than technically.
Premature Event Sourcing
Symptoms. Every domain concept stored as an event stream. Replay is slow. Schema evolution is constant pain. Engineers struggle to answer "what is the current state?" without writing custom queries. Audit log shows technical events (RowUpdated, FieldChanged) rather than business events.
Why it happens. Event sourcing sounds powerful. Teams adopt it for CRUD apps that have no audit requirement, no retroactive analytics need, and no event-shaped domain. The pattern is applied where its benefits do not materialize.
Damage. Operational complexity (schema versioning, snapshots, rebuild procedures), cognitive complexity (engineers thinking in events for problems that are not event-shaped), and the GDPR / right-to-be-forgotten problem with no clear solution.
Escape. Move CRUD entities back to traditional state-based storage. Keep event sourcing only where the audit log is genuinely the product — finance, regulated industries, true event-shaped domains.
Shared Database
Symptoms. Two or more services read and write the same tables. The schema change in one service requires coordination with others. Database migrations are dangerous and rare. Services know the column structure of "shared" tables.
Why it happens. The "microservices" split happens but the database does not split with it. "We'll fix it later." Later is never.
Damage. No service is truly independent. Every schema change is a multi-service coordination. The database is the bottleneck for everything. Teams that own different services cannot evolve their persistence layer. This is the most common form of distributed monolith.
Escape. Database-per-service is the rule. Migrate gradually: pick the next change to a "shared" table; instead of changing it, expose the owning service's API and have the consumer call it. Repeat. This is multi-month work and worth doing.
Anemic Domain Model
Symptoms. Entity classes have only fields and getters/setters. All behavior lives in "service" classes that operate on entities. Business rules are scattered across services with no clear ownership. The same validation logic appears in three places.
Why it happens. ORM-driven design that treats entities as data containers. Pseudo-DDD that adopts the patterns' vocabulary without the substance. Inertia from procedural programming applied in object-oriented languages.
Damage. Business logic has no clear home. Rules are duplicated, drift, get out of sync. Changes ripple across many service classes. The "domain model" is misleading — it does not model the domain, just the database schema.
Escape. Move behavior into entities. Order.cancel() rather than OrderService.cancel(order). Invariants enforced in entity constructors and methods, not in service-layer validation. This is gradual refactoring that takes months on a large codebase. See DDD Tactical Patterns.
Big Ball of Mud
Symptoms. Modules exist on paper but their boundaries are not enforced. Any code can import any other code. Circular dependencies. "It used to be modular when we started." A 30-second review identifies five places where module A reaches into module B's internals.
Why it happens. Modules without build-time enforcement. The discipline rots quickly without tooling. A team that grows past the point where everyone knows the codebase loses the implicit boundaries that previously held.
Damage. Coupling makes change unpredictable. Bug fixes break unrelated features. Onboarding takes months because the system has no internal logic to learn. Performance issues cannot be isolated. Splitting (if eventually attempted) is extremely expensive.
Escape. Introduce a modular monolith gradually. Pick one boundary; declare it; enforce it with build tooling (packwerk, ArchUnit, import-linter, eslint-plugin-import). The first boundary is the hardest; subsequent ones reuse the same tooling.
API Gateway as Monolith
Symptoms. A single API gateway that does authentication, rate-limiting, routing, request transformation, response composition, business validation, audit logging, and feature flag evaluation. The gateway is the largest, most complex, most-frequently-changed service in the system. Outages start in the gateway.
Why it happens. Cross-cutting concerns accumulate naturally at the edge. Without discipline, "while we're at the edge, let's add this too" leads to a gateway that owns everything.
Damage. The gateway becomes a single point of failure (operationally and architecturally). It is a deployment bottleneck. Its complexity exceeds team capacity. It becomes the system's actual monolith, with services behind it functioning as a passive backend.
Escape. Push concerns to their natural homes: business validation into services, response composition into BFFs, audit logging into the services that own the data. Keep the gateway focused on cross-cutting: auth, rate-limit, routing, observability — the things that genuinely belong at the edge.
Synchronous Microservice Chain
Symptoms. Service A calls B which calls C which calls D. The mobile app waits 800ms. A failure in D propagates back through the chain. End-to-end latency is the sum; end-to-end reliability is the product. Retries amplify failures.
Why it happens. Microservices replace function calls with HTTP calls, retaining the synchronous mental model. Teams do not invest in async messaging or domain-event-driven flows.
Damage. Latency grows linearly with chain depth. Reliability shrinks geometrically. Cascading failures: one slow service makes its upstream services slow, which makes their upstream services slow.
Escape. Replace synchronous chains with event-driven flows where the calling service does not need to wait. Use BFF aggregation for cases where the client genuinely needs composed data. Introduce circuit breakers and bulkheads for the synchronous calls that remain.
Premature Microservices
Symptoms. A team of 5 with a 6-month-old product running 12 microservices. Most engineering capacity is consumed by infrastructure work. Features ship slowly. Engineers spend more time on Kubernetes than on the product.
Why it happens. Adopting "microservices" because it is the modern pattern. Splitting before domain understanding. Architectural ambition mismatched to team capacity.
Damage. Velocity collapses. The infrastructure work is real engineering effort that does not produce product features. The bounded contexts get frozen early and become entrenched. Refactoring across services is much harder than refactoring within a monolith.
Escape. Recombine. Pick the services that did not need to be separate and merge them back into a modular monolith. Keep only the splits with measured operational justification. See When to Split.
Resume-Driven Architecture
Symptoms. The system uses a different framework, language, or paradigm for each component. The latest databases. Kubernetes operators for problems a script could solve. New patterns adopted before the previous ones are understood.
Why it happens. Engineers want technology on their resumes. Architectural decisions are driven by individual career incentives rather than system needs. Often paired with high staff turnover where each tenure leaves new infrastructure.
Damage. Operational complexity overwhelms the team. Knowledge is fragmented. Onboarding requires learning a stack rather than a system. The next hire's "stack vision" replaces the previous one without resolving anything.
Escape. Hard problem; political as much as technical. Establish a technology selection process that requires business justification, not just engineering preference. Restrict adoption to one new technology at a time. Document decisions (using ADR) so future hires understand the trade-offs that led to current choices.
Premature Optimization for Scale
Symptoms. A small system with infrastructure designed for Netflix. Sharded everything. Multi-region active-active. Kafka for in-app pub/sub. Service mesh for 5 services. The team spends more on infrastructure than on the product.
Why it happens. Imagining future scale and designing for it instead of designing for present needs. Cargo culting from blog posts written by hyperscale companies whose problems do not match yours.
Damage. Massive operational cost (engineering time, infrastructure bills). Velocity loss. The complexity does not pay back because the predicted scale does not arrive — or if it does, the actual requirements turn out to be different.
Escape. Decommission the unused infrastructure. Run the system on smaller, simpler tools until actual scaling pressure justifies more. Postgres handles more load than people imagine.
Common Themes
Across these anti-patterns, recurring causes:
- Adopting patterns without their preconditions. Microservices without team autonomy. Event sourcing without an audit requirement. Clean Architecture without domain complexity.
- Skipping the strategic work. Bounded contexts not identified. Domain not understood. Team boundaries not designed.
- Fashion-driven choices. Modern patterns adopted because they are modern, not because they fit the problem.
- No discipline to maintain the pattern. The pattern is set up correctly but rots over months without enforcement (build-time checks, code review, automated testing).
- Mismatched team capacity. Architectural ambition exceeds the team's capacity to operate it.
Diagnosing What You Have
Honest signals that you may have an anti-pattern:
- "We can't deploy service A without also deploying B." Distributed monolith.
- "This service owns one table." Microservice-per-table.
- "We can't easily query the current state." Premature event sourcing or wrong storage choice.
- "Two teams are blocked by this database change." Shared database.
- "All the logic is in the service layer." Anemic domain model.
- "Anyone can import anything." Big ball of mud.
- "The gateway does everything." API gateway monolith.
- "This feature took 800ms because of N service hops." Synchronous microservice chain.
- "We have more services than engineers." Premature microservices.
Diagnosis is half the cure. Most of these anti-patterns are recoverable, but recovery requires acknowledging the situation.
Relation to Other Pages
- Monolith — the alternative that is often the right answer.
- Modular Monolith — the middle ground that avoids most of these anti-patterns.
- Microservices — done right; the anti-patterns are microservices done wrong.
- When to Split — the decision process that avoids the premature anti-patterns.
- Conway's Law — many anti-patterns are organizational, not technical.
- DDD Strategic Design — bounded contexts done right avoid many of these.
Further Reading
- Martin Fowler, MicroservicePremium (2015) — argues that microservices have a complexity threshold and most systems should not pay it.
- Sam Newman, Building Microservices (2nd ed., 2021), Chapter 5 — covers distributed monolith and related failures.
- Adam Tornhill, Your Code as a Crime Scene — empirical methods for identifying architectural decay.
- Brian Foote and Joseph Yoder, Big Ball of Mud (1997) — the foundational paper.
- Pat Helland, Life Beyond Distributed Transactions (2007) — the philosophy that, applied carefully, avoids many of these anti-patterns.
Pre-commit Checklist
- Can I deploy each service independently? If no, look at distributed monolith.
- Does each service own a meaningful domain, not just a table? If no, look at microservice-per-table.
- Do entities have behavior, not just fields? If no, look at anemic domain model.
- Are module boundaries enforced by the build, not by convention? If no, big ball of mud is on the way.
- Is the API gateway focused on cross-cutting concerns only? If no, look at gateway-as-monolith.
- For each pattern I have adopted, am I paying its cost because I am getting its benefit — or because it sounded modern?