Skip to content

Reference guide

Architecture Patterns Explained

What these patterns actually mean, why they matter, and how they work together. No jargon walls. Real code. Plain English.

1

Domain-Driven Design (DDD)

Organize by business domain, not technical layer

Most tutorials teach you to organize code by technical layer: all models in one folder, all services in another, all routes in a third. It works for small projects but falls apart fast.

Domain-Driven Design flips this. You organize by business concept. Everything related to scheduling (its models, business logic, routes, tests) lives in one folder. Everything related to billing lives in another.

Layer-based (avoid)

models/        (50 models, 5,000 lines)
services/      (30 services, 4,000 lines)
controllers/   (40 endpoints, 3,000 lines)
schemas/       (60 schemas, 2,000 lines)

Domain-based (prefer)

domains/scheduling/
  models.py      (65 lines)
  service.py     (100 lines)
  router.py      (40 lines)
  tests/         (unit + integration)

Why it matters for AI-augmented development

When you point AI at a layer-based project, it has to parse 16,000 lines where only 2-5% is relevant. With domain-based organization, AI sees ~1,000 lines that are 100% relevant. First-try success jumps from 40% to 88%.

Key rules

  • No cross-domain imports. Domain A never imports domain B's internals. Shared types go in a shared/ directory.
  • Service layer owns business logic. Routes are thin wrappers: they receive HTTP, call the service, return a response.
  • Events for cross-domain communication. If scheduling needs to notify billing, it publishes an event, not a direct import.

Deep dive: Lesson 5: Domain Boundaries


2

Event-Driven Architecture

Domains communicate through events, not direct calls

Imagine deactivating a user. You need to cancel their subscription, send a goodbye email, revoke permissions, log to audit trail, update analytics. In a traditional codebase, your UserService directly calls all five services. Eight dependencies. Eight things to mock in tests.

With events, the UserService publishes one fact: "a user was deactivated." It doesn't know or care who listens. Each subscriber handles its own logic independently.

Direct calls (tight coupling)

class UserService:
    def __init__(self, db, notifications,
        audit, subscriptions, scheduling,
        analytics, cache, permissions):
        # 8 dependencies

Event-driven (loose coupling)

class UserService:
    def __init__(self, db, event_bus):
        # 2 dependencies

    async def deactivate(self, user_id):
        await self.event_bus.publish(
            UserDeactivated(user_id=user_id)
        )

You don't need Kafka

A simple in-process event bus (under 50 lines) handles most applications. You only need dedicated message queues when events must survive process restarts or cross service boundaries.

Key rules

  • Events are past tense. UserDeactivated, InvoiceCreated, OrderShipped. They describe what happened, not what should happen.
  • Events carry IDs, not objects. Keep payloads minimal. Subscribers fetch what they need.
  • Subscribers don't affect the publisher. If a subscriber fails, the original action still succeeds. Handle failures in the subscriber.

Deep dive: Lesson 6: Events Over Calls


3

Monolith-First

Start simple, extract services when you have proof

"Should I use microservices?" is the wrong first question. The right question is: "Do I have proof that any domain needs to scale independently?" If the answer is no (and for most early-stage projects it is), start with a monolith.

A well-structured monolith with domain boundaries gives you 90% of microservice benefits with a fraction of the operational complexity. No service mesh. No distributed tracing headaches. No "which service is failing at 3 AM" puzzles.

When to extract a service

Extract a domain into a separate service only when you have concrete proof of one of these:

Independent scaling

The domain handles 100x more load than the rest of the application.

Different release cadence

The domain needs to deploy independently, multiple times per day.

Different tech stack

The domain requires a language or framework the monolith doesn't use.

Key rules

  • API-first within the monolith. Domains communicate through defined interfaces, even inside one process. This makes future extraction straightforward.
  • Domain boundaries are the preparation. If you follow DDD and event-driven communication, extracting a domain into a service becomes a deployment change, not a rewrite.
  • One repo, multiple domains. Keep everything in a monorepo. Split by domain folder, not by repository.

Related: Lesson 5: Domain Boundaries


4

Row-Level Security (RLS)

Multi-tenant isolation enforced by the database

In a multi-tenant application, multiple customers (tenants) share the same database. The critical question: how do you make sure Tenant A never sees Tenant B's data?

Most applications solve this with application-level filters, adding WHERE tenant_id = X to every query. But one missed filter, one forgotten check, one developer who doesn't know about it, and you have a data leak.

Row-Level Security is a PostgreSQL feature that moves this check into the database itself. The filter is automatic and cannot be bypassed by application code.

How it works

-- 1. Enable RLS on the table
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 2. Create a policy: filter by current tenant
CREATE POLICY tenant_isolation ON projects
  FOR ALL TO app_user
  USING (
    tenant_id = current_setting('app.current_tenant_id')::uuid
  );

-- 3. Set context in your middleware (every request)
SET app.current_tenant_id = 'tenant-uuid-here';
SET ROLE app_user;

-- Now every query is automatically filtered.
-- SELECT * FROM projects returns only this tenant's data.

The fail-safe default

What happens when the session variable isn't set? With RLS, the answer is: zero rows returned. No context, no data. This is the opposite of application-level checks where the default is "return everything unless something stops it."

Key rules

  • Enforce at the database, not the application. Application bugs can't bypass database-level policies.
  • Fail closed. If session variables aren't set, the policy returns zero rows, not all rows.
  • Test isolation in CI. Create two tenants, insert data for both, verify tenant A cannot query tenant B's data.
  • Use CASE statements in policies to safely handle empty session variables without UUID casting errors.

Deep dive: Lesson 7: Security by Architecture


5

Quality Gates

Three automated layers that catch 87% of bugs

Fixing a bug at commit time takes 2 minutes. Fixing the same bug in production takes 40+ hours. That's a 1,200x cost difference. Quality gates are automated checkpoints that catch problems as early as possible.

Layer 1 < 5 seconds

Pre-commit

Formatting, linting, secret detection. Runs on every save.

Layer 2 < 60 seconds

Pre-push

Type checking, unit tests, security-focused lint. Runs before code reaches the remote.

Layer 3 < 5 minutes

CI/CD

Full test suite, coverage enforcement (80%+), dependency audit. Runs on every pull request.

Speed budgets are non-negotiable

The moment a quality check is slow, developers bypass it. Pre-commit hooks that take 30 seconds? Developers add --no-verify to muscle memory. Strict speed budgets keep the system honest.

Key rules

  • Each layer has a speed budget. Pre-commit: under 5s. Pre-push: under 60s. CI: under 5 minutes.
  • Fail fast. Stop at the first error. Don't run slow checks when fast checks already failed.
  • Secret detection is non-optional. Catching a leaked API key before commit costs 0 effort. Catching it after push costs a rotation and an audit.

Deep dive: Lesson 8: Quality Gates · Get the configs: Pattern Composer


6

Type Safety Pipeline

One schema, zero drift, zero runtime type errors

Your backend defines an API. Your frontend consumes it. Between them is a gap where types can drift. You rename a field in Python, but the TypeScript client still uses the old name. It compiles fine. It deploys. Then it breaks at 2 AM.

A type safety pipeline eliminates this gap. One schema definition, automatically propagated to all consumers.

The pipeline

Pydantic / Zod OpenAPI spec TypeScript types

Change a field in the backend schema. Run one command. Frontend types update automatically. TypeScript compiler catches every broken reference at build time, not runtime.

Key rules

  • Never hand-write API client types. Generate them from the OpenAPI spec. Manual type definitions drift within weeks.
  • Ban any. Enable strict: true in tsconfig. Every any is a hole in your type safety net.
  • Run the pipeline in CI. If generated types differ from committed types, the build fails. No one ships stale types.

Deep dive: Lesson 4: Type Safety Pipeline


How these patterns connect

Domain boundaries give AI focused context (1,000 lines instead of 16,000).

Events keep domains independent. No circular imports, no 8-dependency constructors.

Type safety ensures changes propagate without drift, from backend to frontend.

RLS enforces security at the foundation so you can move fast without fear.

Quality gates catch 87% of bugs automatically before any human reviews code.

A monolith-first approach keeps operational complexity low while you build.

Each pattern reinforces the others. The more you adopt, the faster and safer your development becomes.

Frequently asked questions

What is Domain-Driven Design (DDD) in simple terms?
DDD means organizing your code around business concepts instead of technical layers. Instead of putting all models in one folder and all services in another, you group everything related to "scheduling" or "billing" together. Each domain folder contains its own models, logic, routes, and tests, making it self-contained and easy for both humans and AI to understand.
Do I need microservices to use Domain-Driven Design?
No. DDD works perfectly inside a monolith. You organize by domain within a single codebase. Microservices are an infrastructure decision you make later, only when a specific domain needs independent scaling or a different tech stack.
What is event-driven architecture and when should I use it?
Event-driven architecture means domains communicate by publishing events ("this happened") instead of calling each other directly. Use it when one action triggers multiple side effects, like a user signup that needs to send a welcome email, create a billing record, and log analytics. Events let you add new side effects without modifying the original code.
Do I need Kafka or RabbitMQ for event-driven architecture?
Not for most applications. A simple in-process event bus (under 50 lines of code) handles the vast majority of use cases. You only need dedicated message queues when events need to survive process restarts, or when different domains run as separate services.
What is Row-Level Security (RLS) and why does it matter?
RLS is a PostgreSQL feature that automatically filters database queries based on the current user or tenant. Instead of adding "WHERE tenant_id = X" to every query in your application code, the database enforces it for you. Even if your application has a bug, data from other tenants is invisible. It is the strongest form of multi-tenant data isolation.
What is the monolith-first approach?
Start with a well-structured monolith instead of jumping to microservices. Keep all your code in one repository, organize it by domain, and only extract a domain into its own service when you have concrete proof it needs independent scaling. This gives you 90% of microservice benefits with a fraction of the operational complexity.
What are quality gates and how do they catch bugs automatically?
Quality gates are three layers of automated checks: pre-commit hooks (under 5 seconds, catch formatting and secrets), pre-push hooks (under 60 seconds, catch type errors and failing tests), and CI/CD pipelines (under 5 minutes, run full test suites with coverage). Together they catch 87% of bugs before any human reviews code.
How do these patterns work together?
They form a compounding system. Domain boundaries give AI focused context. Events keep domains independent. RLS enforces security at the foundation. Quality gates catch mistakes automatically. Each pattern reinforces the others. The more you adopt, the faster and safer your development becomes.

Ready to apply these patterns?

Start the free course