In the age of AI programming, there is one kind of code that gets copied around with abandon:
logger.info("Start processing request")
if not token:
raise UnauthorizedError()
start = time.time()
result = await service.do_something()
logger.info(f"Execution time: {time.time() - start}")
return result
At first there is only one endpoint, and it looks harmless enough.
But once the project grows to dozens or even hundreds of endpoints, things quickly become:
User endpoints
├─ Authentication
├─ Logging
├─ Parameter validation
├─ Business logic
└─ Instrumentation
Agent endpoints
├─ Authentication
├─ Logging
├─ Parameter validation
├─ Business logic
└─ Instrumentation
Knowledge base endpoints
├─ Authentication
├─ Logging
├─ Parameter validation
├─ Business logic
└─ Instrumentation
...
The only truly different part is the few lines of business logic in the middle. As for the rest, every time the AI writes another endpoint, it dutifully copies it all over again.
This is exactly the problem that AOP (Aspect-Oriented Programming) set out to solve.
📌 Technology Card
AOP / Aspect-Oriented Programming
A software design philosophy that extracts logic shared by many business modules out of the specific business code and handles it in one place.
This shared logic is commonly referred to as:
Cross-Cutting Concerns
Typical examples include: logging, authentication, authorization, auditing, performance metrics, tracing, caching, transactions, instrumentation, and exception handling.
The core of AOP is not any particular framework or special syntax, but a very simple architectural principle:
Business code handles business; shared rules handle shared rules.
💡 An Intuitive Way to Understand It: AOP Is Like Airport Security
Imagine an airport with many boarding gates.
The Beijing flight has one gate:
Security check
↓
ID check
↓
Register information
↓
Board the plane
The Shanghai flight is the same:
Security check
↓
ID check
↓
Register information
↓
Board the plane
…
If we followed the approach many AIs take when writing code, it would probably turn into:
Every gate gets its own security checkpoint equipment.
Obviously unreasonable. What happens in the real world is:
All passengers go through the unified security checkpoint first, and then proceed to their own gates.
Software is no different.
Without cross-cutting governance:
Router A
├─ Logging
├─ Token validation
├─ Permission check
├─ Business code
└─ Performance metrics
Router B
├─ Logging
├─ Token validation
├─ Permission check
├─ Business code
└─ Performance metrics
After extraction:
Logging / Request ID
Performance / Audit"] B --> C["Authentication
Identity / Permissions"] C --> D[Router] D --> E[Service] E --> F[Repository]
The Router can then refocus on a single job:
Handling business.
💡 Don’t Mistake AOP for “You Must Use an AOP Framework”
When many developers first encounter AOP, they think of Spring AOP in Java.
For example:
@Before
@After
@Around
But that is just one implementation of AOP. What truly matters about AOP is:
Identifying cross-cutting concerns and extracting them from business code.
In a Python + FastAPI project, you can perfectly well use:
Middleware
Dependency Injection
Decorator
ContextVar
Unified exception handling
to achieve the same architectural goal. For example, miniagent currently takes exactly this approach.
The core directories of the repository already clearly place general-purpose capabilities in app/core, which contains:
core/
├── audit_context.py
├── deps.py
├── logger_config.py
├── security/
│ ├── auth_permission.py
│ ├── jwt_auth.py
│ └── ...
└── service_container.py
rather than scattering authentication, logging, and audit logic across every business Router.
This is, in fact, a very Python/FastAPI-flavored realization of AOP thinking.
1. Architectural Rules: Where Should Cross-Cutting Logic Live?
For a FastAPI project, you can give the AI a very simple decision standard.
Global request-level logic → Middleware
Middleware is suitable for concerns that every HTTP request may involve.
For example:
Request logging
Request ID
Endpoint latency
Global auditing
Tracing
Unified headers
The basic structure:
@app.middleware("http")
async def middleware(request, call_next):
# Before the request
...
response = await call_next(request)
# After the request
...
return response
The idea behind it is:
Before
↓
Request → Middleware → Router
↑
After
That is, the classic AOP pattern of:
Uniformly inserting shared behavior before and after business logic executes.
2. The Real Implementation in miniagent
The diagram below fairly completely illustrates the AOP approach of miniagent:

In the current main.py, after a request enters the unified logging middleware, a request_id is created, the start time is recorded, and an audit context is established — and only then is the actual Router invoked. After the request completes, the status code, latency, and audit results are logged uniformly, and X-Request-ID is returned to the client.
At the same time, AuthPermission centrally handles JWT verification, user resolution, user status checks, permission caching, and permission decisions, and writes identity information into the audit context.
And audit_context.py uses a ContextVar to hold the current request’s:
request_id
method
path
ip_address
user_id
username
change_count
so that this information propagates along the async call chain without being passed down layer by layer as function parameters.
Together, these three components form:
Middleware
+
Dependency / Permission
+
ContextVar
a three-layer cross-cutting system.
This is not the “heavyweight AOP” traditionally implemented via complex proxy mechanisms.
Instead, it fits the character of a FastAPI project better:
Implementing AOP thinking with the framework’s native capabilities.
1. Logging Is Not Written by Every Endpoint
In miniagent’s main.py there is a unified HTTP request logging middleware:
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Record all HTTP requests and inject request_id into every log line."""
request_id = str(uuid4())
request.state.request_id = request_id
start_time = time.time()
with logger.contextualize(request_id=request_id):
logger.info(
f"📥 {request.method} {request.url.path}"
)
response = await call_next(request)
process_time = time.time() - start_time
logger.info(
f"📤 {request.method} {request.url.path} "
f"- {response.status_code} ({process_time:.3f}s)"
)
response.headers["X-Process-Time"] = str(process_time)
response.headers["X-Request-ID"] = request_id
return response
This is an excerpt based on the current repository code; the actual implementation also handles exceptions, auditing, and login logging.
The change it brings is significant. Before, you might have:
@router.get("/users")
async def list_users():
logger.info("GET /users start")
start = time.time()
result = await user_service.list_users()
logger.info(
f"GET /users finished: {time.time() - start}"
)
return result
Now the Router can simply be:
@router.get("/users")
async def list_users():
return await user_service.list_users()
Because questions like:
Who made the request?
Which endpoint was accessed?
When did it start?
When did it end?
What was the status code?
How long did it take?
What is the Request ID?
no longer belong to the Router — the Middleware takes care of them uniformly.
This is what it means to:
Cut cross-cutting logic out of business code.
2. Identity and Permissions → Dependency
Not all cross-cutting logic is suitable for Middleware.
For example:
/user/profile
only requires being logged in.
Whereas:
/admin/users
may require the system:user:list permission.
Or, for instance:
/admin/users/{id}
may require the system:user:delete permission when deleting a user.
Clearly, you can’t simply have one global Middleware decide every business permission.
That’s where FastAPI’s:
Dependency Injection
comes in, i.e.:
Depends(...)
3. Authentication: Not Every Router Re-Parses JWT
miniagent implements a unified AuthPermission, responsible for:
JWT verification
↓
Resolve the user
↓
Check user status
↓
Load the permission set
↓
Permission check
JWT (JSON Web Token) is a common token format for authentication.
Rather than having every endpoint write its own:
token = request.headers.get("Authorization")
username = verify_token(token)
user = await find_user(username)
permissions = await load_permissions(user.id)
if permission not in permissions:
raise HTTPException(...)
miniagent encapsulates this whole flow inside AuthPermission.
For example, the core permission-check logic is very clean:
async def check(
self,
user_id: int,
required: str,
) -> None:
perms = await self.get_permissions(user_id)
if SUPER_PERMISSION in perms or required in perms:
return
raise_forbidden(
"auth.permission_denied",
required=required,
)
The permission data itself is also cached, avoiding a fresh database read on every request.
This way, the business layer only needs to express:
What permission this endpoint requires.
Instead of re-implementing:
How the permission system actually works.
4. Declare Rules, Don’t Repeatedly Implement Them
This is a very important point of AOP thinking.
Good business code should be as close as possible to:
@router.delete("/users/{user_id}")
async def delete_user(...):
return await user_service.delete(user_id)
while declaring, via a Dependency, Decorator, or other unified mechanism:
Requires login
Requires system:user:delete permission
instead of:
@router.delete("/users/{user_id}")
async def delete_user(...):
# Parse Authorization
...
# Verify JWT
...
# Look up user
...
# Look up permissions
...
# Check super admin
...
# Write access log
...
# Record start time
...
# ===== The real business finally begins =====
await user_service.delete(user_id)
# ===== Business ends =====
# Write audit log
...
# Write timing log
...
return ...
The biggest danger of the latter is not the code length.
It is:
AI is exceptionally good at copying this kind of code.
And then copying it dozens of times.
5. Wrapping Permissions as a Callable Dependency
AuthPermission also contains a design well worth borrowing for AI projects:
class Permission:
def __init__(
self,
permission_code: str,
) -> None:
self._code = permission_code
async def __call__(
self,
request: Request,
credentials = Depends(_bearer_scheme),
) -> int:
auth = request.app.state.container.auth
user_id = await auth.resolve_user_id(
credentials.credentials
)
await auth.check(
user_id,
self._code
)
return user_id
This way, the permission component itself becomes a dependency object FastAPI can recognize.
The whole relationship can be understood as:
Router
│
│ Declares the permission
↓
Permission("system:user:delete")
│
↓
AuthPermission
│
├── Verify JWT
├── Resolve User
├── Load Permission
└── Check Permission
The Router doesn’t need to understand:
How is a JWT parsed?
How is the user looked up?
Where is the permission cache?
How is a super admin determined?
It is only responsible for expressing:
I want system:user:delete
This is a fundamentally important architectural idea:
The business layer declares intent; the infrastructure layer implements the mechanism.
6. ContextVar Solves Cross-Layer Context Passing
Logging and auditing face another troublesome problem.
Suppose the call chain is:
HTTP Request
↓
Router
↓
Service
↓
Repository
↓
Database
If every layer needs:
request_id
user_id
username
The most straightforward approach might be:
service.run(
request_id=request_id,
user_id=user_id,
...
)
And then:
repository.save(
request_id=request_id,
user_id=user_id,
...
)
This quickly pollutes every function signature.
miniagent uses the following for its audit context:
ContextVar (Context Variable)
The code defines:
_audit_context: ContextVar[
Optional[AuditRequestContext]
] = ContextVar(
"audit_request_context",
default=None
)
The context is established when the request enters:
begin_audit_context(...)
And when the request ends:
reset_audit_context(...)
After authentication succeeds, it calls:
set_audit_user(
user.id,
user.username
)
to attach the user identity to the audit context of the current request.
So the whole flow becomes:
HTTP Request
↓
Middleware
│
├── request_id
├── method
├── path
└── ip
↓
Audit Context
↓
AuthPermission
│
├── user_id
└── username
↓
Audit Context
↓
Service / Repository
This is another kind of cross-cutting capability:
The request context propagates along the entire async call chain without polluting business function parameters.
3. What Are the Actual Benefits of AOP?
1. Less Duplicate Code
The most direct change:
100 endpoints
no longer means:
100 copies of logging code
100 copies of auth code
100 copies of timing code
100 copies of audit code
Shared rules are implemented exactly once.
2. Changing a Rule Means Changing It in One Place
Suppose the logs later need to add:
request_id
client_ip
user_id
latency
If logging is scattered across 200 endpoints:
Cost of change ≈ 200 edits
With Middleware:
Cost of change ≈ 1 edit
This is the problem architecture truly solves.
3. Business Code Becomes Easier to Read
A good Router should let you see at a glance:
What request it accepts
Which Service it calls
What result it returns
instead of first wading through dozens of lines of:
token
logger
permission
cache
metrics
audit
before finding the actual business.
4. Shared Rules Stay Consistent
If you let the AI write authentication 50 times, you may end up with 50 subtly different versions.
For example:
if permission not in permissions:
Another one:
if required_permission not in permissions:
Another forgets the super admin:
if required not in permissions:
Another forgets to check whether the user is disabled.
One of the values of AOP is:
Giving every shared rule a single authoritative implementation.
4. Establish Explicit AOP Architecture Rules for the AI
You can write the following directly into your Project Rules:
## Cross-Cutting Concerns
Logging, authentication, authorization, auditing, metrics, tracing, and exception handling are cross-cutting concerns.
It is forbidden to implement this logic repeatedly in Routers or business Services.
Unified rules:
- Global HTTP request logic uses Middleware;
- Authentication and permission checks prefer FastAPI Depends;
- Use Decorators only when dependency injection is unsuitable;
- Request-scoped context uses ContextVar;
- Exception and response conversion uses global exception handlers;
Routers are only responsible for request orchestration;
Services are only responsible for business logic;
Repositories are only responsible for data access.
These few lines of rules constrain the AI far more effectively than:
Please write high-quality code
Don’t Tell the AI to “Add Some Auth”
If you simply say:
Add permission control to this endpoint.
The AI will very likely invent a permission system on the spot. A better prompt is:
Please add permission control to this endpoint.
Requirements:
1. Do not parse JWT in the Router;
2. Do not re-implement permission checks;
3. Use the project's existing AuthPermission;
4. Perform the permission check via FastAPI Depends or the existing Permission mechanism;
5. The Router only declares the required permission;
6. Reuse the existing auth singleton in the ServiceContainer;
7. Do not alter the existing unified exception system.
This effectively tells the AI:
Do not invent mechanisms; only use the mechanisms the project already has.
Logging Requirements Should Be Given to the AI the Same Way
Bad prompt:
Add access logging to every endpoint.
The AI will very likely modify dozens of Routers directly:
logger.info(...)
Better prompt:
We need to add HTTP request access logging.
First determine whether this feature is a cross-cutting concern.
Requirements:
1. Do not modify Routers one by one;
2. Implement it uniformly with FastAPI Middleware;
3. Generate a request_id for every request;
4. Record method, path, status_code, and latency;
5. Return X-Request-ID in the response;
6. The logging context should propagate automatically to other modules within the current request;
7. Do not pollute business parameters in Services and Repositories.
This sentence:
First determine whether this is a cross-cutting concern
is well worth adding to your AI project rules.
Don’t Let Instrumentation Turn Business Code into a “Christmas Tree”
Many systems later add:
User clicks
Agent invocation counts
LLM token consumption
Endpoint latency
Knowledge base hit rate
Tool success rate
If all of it is written inline in the business code:
metrics.inc("agent_call")
logger.info(...)
tracer.start(...)
audit.record(...)
result = await agent.run()
metrics.observe(...)
logger.info(...)
tracer.end(...)
in the end, the actual business logic shrinks to:
result = await agent.run()
Code like this still runs, but it shows the cross-cutting logic has intruded too deeply into the business.
The healthier direction looks like:
Metrics
Logging
Tracing
Audit
│
│ Cutting across horizontally
↓
────────────────────
Router → Service
────────────────────
rather than:
Router
↓
Metrics
↓
Logging
↓
Tracing
↓
Auth
↓
Audit
↓
Business
Summary
AOP is not mysterious. The essential problem it solves is:
Extracting the code that is “needed everywhere, but doesn’t truly belong to any single business” out of the business logic.
Logging, authentication, auditing, instrumentation, performance metrics, and tracing are all classic cross-cutting concerns.
For a FastAPI project, there is no need to introduce a complex framework just to chase the name “AOP”.
A very practical combination is enough:
Middleware
+
Dependency Injection
+
Decorator
+
ContextVar
+
Global Exception Handler
miniagent currently follows exactly this direction.
And in AI programming, we should go one step further and write it down as an explicit project rule:
When you discover duplicated cross-cutting logic, do not keep copying. Stop first and judge whether it should be extracted into Middleware, a Dependency, a Decorator, or unified infrastructure.
Ultimately, what we really want from AI is not:
Every file looks pretty on its own.
but:
The whole project, taken together, still looks like it was designed by one person.
That is the true value of architecture in the age of AI programming.
Open Source Code
🪐 Good luck 🪐