Back to blog
System Design2026-05-285 min read

Common Architecture Mistakes We Found Across 100+ Code Submissions

A field guide to the design patterns that trip up experienced developers

A
Al pacino
Agentic AI Engineer, Taqeem
#Architecture#Backend#System Design#Engineering

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:

  1. Enforcing layered architecture at the framework level (not just convention)
  2. Making error handling explicit in code reviews (not optional)
  3. Using dependency injection from the start (refactoring later is expensive)
  4. Standardizing external service patterns (retry, timeout, circuit breaker)
  5. Requiring environment-based configuration from day one

Want to see how your code scores? Start a free assessment →

For Developers

Prove your engineering skills

Take a real coding evaluation, skip resume screens, and get discovered by top hiring teams.

Get Verified for Free

100% Free · Privacy Protected

For Hiring Managers

Hire pre-vetted senior engineers

Access vetted candidate profiles with multi-judge AI scoring and verified code samples.

Browse Verified Talent

Zero Placement Fees · Instant Access