Skip to main content
Clean Architecture 60 XP · 6 min

The Main Component: The Ultimate Detail

Main is the dirtiest, lowest-level component — the entry point that wires all dependencies and hands control to the clean architecture above it.

Showing
Ad (728×90)

Why this matters

Main is the lowest-level policy module in the system — it is a plugin to the application. Its job is to create all the concrete implementations, inject them into the abstract interfaces the inner layers depend on, and then pass control inward. Main is intentionally dirty: it knows about Flask, SQLAlchemy, Stripe, environment variables, and every other concrete detail. That is its purpose.

The key insight: Main should be thought of as an outer circle in Clean Architecture — it knows about everything, but nothing inside knows about Main. This means you can have multiple Main components. A production Main wires real repositories and real payment providers. A test Main wires in-memory stubs. A staging Main wires mock services. The inner layers are completely unaware of which Main was used.

When business logic is scattered at module level alongside framework setup, it becomes impossible to swap the infrastructure. All the wiring, configuration, and business logic become inseparable. Main forces you to make the composition explicit — and that explicitness is what makes your architecture testable and maintainable.

The problem

Framework setup and business logic mixed at module level — impossible to test the business logic without starting Flask and connecting to MySQL.

Bad

# app.py — business logic and infrastructure inseparable at module level
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://user:pass@localhost/orders"
db = SQLAlchemy(app)

class Order(db.Model):  # ORM model is also the domain entity
    id     = db.Column(db.Integer, primary_key=True)
    total  = db.Column(db.Float)

    def calculate_tax(self):
        return self.total * 0.21  # business rule tangled with ORM

@app.route("/orders/<int:order_id>")
def get_order(order_id):
    order = Order.query.get_or_404(order_id)
    return jsonify({"id": order.id, "tax": order.calculate_tax()})

if __name__ == "__main__":
    app.run()

# Cannot test calculate_tax() without Flask and MySQL running.
// index.ts — composition scattered, business logic and infrastructure mixed
import express        from "express";
import { DataSource } from "typeorm";

const ds = new DataSource({ type: "mysql", host: "localhost", database: "orders",
  entities: [__dirname + "/entities/*.ts"], synchronize: true });

const app = express();
app.get("/orders/:id", async (req, res) => {
  await ds.initialize();
  const order = await ds.getRepository(OrderEntity).findOneBy({ id: req.params.id });
  if (!order) return res.status(404).json({ error: "not found" });
  const tax = order.total * 0.21;  // business rule buried in handler
  res.json({ id: order.id, tax });
});

app.listen(3000);
// Impossible to test the tax rule without starting the full server.

The solution

main.py wires the concrete implementations and injects them into the inner layers — which know nothing about Flask or MySQL. For tests, inject in-memory stubs instead.

Good

# order.py (inner layer — no Flask, no SQLAlchemy)
from dataclasses import dataclass
from decimal import Decimal

@dataclass
class Order:
    order_id: str
    total: Decimal

    def calculate_tax(self) -> Decimal:
        return self.total * Decimal("0.21")

# order_service.py (inner layer)
class OrderService:
    def __init__(self, repo):  # abstract interface
        self._repo = repo

    def get_order_with_tax(self, order_id: str):
        order = self._repo.find_by_id(order_id)
        return {"id": order.order_id, "tax": str(order.calculate_tax())}

# main.py — the ONLY file that knows about Flask and MySQL
from flask import Flask, jsonify
from mysql_order_repository import MySQLOrderRepository
from order_service import OrderService

def create_app() -> Flask:
    repo    = MySQLOrderRepository("mysql://user:pass@localhost/orders")
    service = OrderService(repo)
    app     = Flask(__name__)

    @app.route("/orders/<order_id>")
    def get_order(order_id):
        return jsonify(service.get_order_with_tax(order_id))

    return app

if __name__ == "__main__":
    create_app().run()

# Tests: inject InMemoryOrderRepository — no Flask, no MySQL needed.
// Order.ts (inner layer — no imports from outer layers)
export class Order {
  constructor(readonly orderId: string, readonly total: number) {}
  calculateTax(): number { return this.total * 0.21; }
}

// OrderRepository.ts (abstract port)
export interface OrderRepository { findById(id: string): Promise<Order | null>; }

// GetOrderUseCase.ts (inner layer)
export class GetOrderUseCase {
  constructor(private readonly repo: OrderRepository) {}
  async execute(orderId: string) {
    const order = await this.repo.findById(orderId);
    if (!order) return null;
    return { id: order.orderId, tax: order.calculateTax() };
  }
}

// main.ts — the ONLY file that knows about Express and TypeORM
import express from "express";
import { TypeOrmOrderRepository } from "./TypeOrmOrderRepository";
import { GetOrderUseCase }        from "./GetOrderUseCase";

const repo    = new TypeOrmOrderRepository(dataSource);
const useCase = new GetOrderUseCase(repo);

const app = express();
app.get("/orders/:id", async (req, res) => {
  const result = await useCase.execute(req.params.id);
  if (!result) return res.status(404).json({ error: "not found" });
  res.json(result);
});
app.listen(3000);
// Tests: inject InMemoryOrderRepository — no Express, no TypeORM, no DB.

Key takeaway

Main is the only component allowed to know about concrete classes. It wires everything together and passes control inward. Everything inside depends only on abstractions. With multiple Main components — production, test, staging — the same business logic runs without any framework being loaded. Main is a detail; the architecture above it is the design.

Done with this lesson?

Mark it complete to earn XP and track your progress.