Common Architecture Mistakes We Found Across 100+ Code Submissions
A field guide to the design patterns that trip up experienced developers
How We Gathered This Data
Every submission to Taqeem is evaluated by three LLM judges who provide structured feedback with file:line evidence. We analyzed the "whatwentwrong" and "missing_patterns" sections from 100+ evaluations to find the most common architectural mistakes.
Methodology
We categorized each mistake by type and severity. Results are from real (not toy) submissions — candidates were solving actual engineering tasks like implementing caching layers, building REST APIs, fixing production bugs, and designing notification systems.
#1: Business Logic in the Wrong Layer (37% of submissions)
The Mistake
class OrderService:
def create_order(self, request: Request) -> Response:
# Business logic mixed with HTTP concerns
if not request.user.is_authenticated:
return Response(status=401)
# Validation logic inline
if request.data.get("total", 0) < 0:
return Response(status=400)
# Database logic inline
order = Order(total=request.data["total"])
db.session.add(order)
db.session.commit()
# Email sending inline
send_email(request.user.email, "Order created")
# Logging inline
logger.info(f"Order created: {order.id}")
return Response(order.to_dict(), status=201)
Why It's Wrong
This violates the Single Responsibility Principle at every layer. The service layer handles HTTP, validation, persistence, notification, and logging. Changing any one of these concerns risks breaking the others.
The Fix
# services/order_service.py
class OrderService:
def __init__(self, repo: OrderRepository, notifier: NotificationService):
self._repo = repo
self._notifier = notifier
def create_order(self, data: CreateOrderDto) -> Order:
order = Order(total=data.total, user_id=data.user_id)
self._repo.save(order)
self._notifier.order_created(order)
return order
Layered Architecture
Separate HTTP handling → validation → business logic → persistence → external services. Each layer has one job.
#2: Missing Error Handling on Database Calls (31% of submissions)
The Mistake
def get_user(user_id: int) -> User:
user = db.session.query(User).get(user_id)
return user # Can return None! Caller doesn't know.
Why It's Wrong
Unhandled None values propagate silently. The caller either crashes with an AttributeError or, worse, passes None through multiple layers before failing in an unrelated part of the system.
The Fix
def get_user(user_id: int) -> User:
user = db.session.query(User).get(user_id)
if user is None:
raise UserNotFoundError(f"User {user_id} not found")
return user
Fail Fast
Returning None from a lookup function pushes the error handling responsibility to every caller — and most callers won't check.
#3: Tight Coupling Through Direct Instantiation (28% of submissions)
The Mistake
class ReportGenerator:
def __init__(self):
# Direct dependency — can't test, can't swap
self.db = DatabaseConnection()
self.cache = RedisCache()
self.email = EmailService()
The Fix
class ReportGenerator:
def __init__(
self,
db: DatabaseConnection,
cache: CacheService,
email: EmailService,
):
# Injected dependencies — testable, swappable
self.db = db
self.cache = cache
self.email = email
#4: Missing Retry Logic for External Services (24% of submissions)
The Mistake
def send_notification(user_id: int, message: str):
response = requests.post(
"https://push.example.com/send",
json={"user_id": user_id, "message": message}
)
response.raise_for_status()
Network calls fail. Services go down. This code crashes on the first transient failure.
The Fix
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
def send_notification(user_id: int, message: str):
response = requests.post(
"https://push.example.com/send",
json={"user_id": user_id, "message": message},
timeout=5,
)
response.raise_for_status()
#5: Hardcoded Configuration (19% of submissions)
The Mistake
# config.py — wait, no, this is production code
DB_HOST = "localhost"
DB_PORT = 5432
DB_NAME = "myapp_dev"
API_KEY = "sk-1234567890abcdef"
Why It's Wrong
Hardcoded config means:
- Can't deploy to different environments without code changes
- Secrets in code — every commit exposes credentials
- No audit trail — who changed what config when?
The Fix
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "myapp"
api_key: str
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
The Patterns by Seniority
We also broke down these mistakes by the candidate's classified level:
| Anti-pattern | Junior | Mid-Level | Senior | Staff |
|---|---|---|---|---|
| Business logic in wrong layer | 52% | 41% | 28% | 12% |
| Missing error handling | 48% | 35% | 22% | 8% |
| Tight coupling | 38% | 31% | 24% | 15% |
| Missing retry logic | 44% | 28% | 19% | 11% |
| Hardcoded config | 42% | 24% | 14% | 3% |
The gap between mid-level and senior engineers isn't about writing more complex code — it's about writing simpler code that handles failure gracefully.
What This Means for Your Team
These patterns suggest that most engineering teams could significantly improve code quality by:
- Enforcing layered architecture at the framework level (not just convention)
- Making error handling explicit in code reviews (not optional)
- Using dependency injection from the start (refactoring later is expensive)
- Standardizing external service patterns (retry, timeout, circuit breaker)
- Requiring environment-based configuration from day one
Want to see how your code scores? Start a free assessment →