从一个“登录计数”问题谈分层架构的设计原则与实践技巧 From a “Login Counting” Question to Principles and Practical Techniques of Layered Architecture Design

从一个“登录计数”问题谈分层架构的设计原则与实践技巧

By Geoffrey Chen

在进行分层架构的设计与开发时,我们经常会遇到一些看似细微、却极容易引发混淆的问题。这些问题往往表面上是“代码应该放在哪一层”,但深入思考后会发现,它们实际触及的是架构分层的语义边界。

以我们开发的 Smallsoft Identity(SI) 为例,就遇到了这样一个看似简单、却非常有启发性的具体问题:

用户登录成功之后,登录次数(Login Usage)应该在哪里统计?是在 Api 层,还是在 Application 层?乍看之下,这似乎只是一个实现细节;但在深入讨论之后,它实际上揭示了分层架构中一个非常核心、却经常被忽略的设计原则。

表象上的“合理”:在 Api 层统计登录

从代码结构上看,把登录统计放在 Api 层是一个非常自然的想法:

var result = await _login.ExecuteAsync(request);

if (result.Success)
{
// 在这里统计一次登录?
}

Api 层接收 HTTP 请求,调用 Application 层的 LoginUserCommand,并得到了一个 Success = true 的返回结果。

直觉很容易得出这样的结论:“登录成功了,那就在这里统计一次登录吧。” 事实上,这种做法在不少系统中都很常见。但问题在于——这种做法在架构语义上是错误的。

关键分歧点:Api 知道“成功”,但不知道“发生了什么” Api 层真正知道的事实只有一个:“某个请求成功返回了。”

但 Api 层并不知道、也不应该去判断以下这些关键的业务语义事实:
• 是否创建了一个新的认证会话
• 是否复用了已有的 Session
• 是否属于静默认证或自动登录
• 将来 LoginUserCommand 的内部实现是否会发生变化

换句话说:

Api 层看到的是“控制流成功”,而不是“业务状态发生了什么变化”。而“登录”本身,恰恰不是一个控制流概念,而是一个业务状态变化。

真正的“登录”发生在哪里?在 SI 的实现中,“登录”真正发生在这一刻:

await _sessions.CreateSessionAsync(new Session { … });

这一行代码所表达的业务语义非常清晰:

“一个新的、已认证的用户会话被创建了。”

这是一个明确的、不可歧义的、不可回退的业务事实。而这一事实只存在于 Application 层的用例逻辑中,而不是 Api 层。因此,一个关键结论自然浮现出来:Login 不是 Api 事件,而是 Application 事件。

一个可以反复使用的分层原则

由此,我们可以总结出一个非常实用、且可以在很多系统中反复应用的分层原则:

如果一个行为代表的是“业务事实已经成立”,那么它应该在 Application 层被确认和记录;Api 层只能负责请求转发与结果返回,而不拥有业务语义的裁量权。

这个原则的反面同样清晰:
• Api 层:适合处理协议级、传输级、表现级事件
• Application 层:适合处理业务完成、状态转换、事实成立

为什么 Access Token 的计数可以放在 Api 层?

在同一个系统中,我们确实选择了在 OAuth Controller(Api 层)统计 Access Token 的发放次数。这并不矛盾。原因在于:
• Access Token 是 OAuth 协议的直接产物
• Token“被发放”这一事实,只在 OAuth Controller 中成立
• 当前系统中并不存在一个协议无关的 IssueAccessToken 业务用例

因此:
• Login = 业务事实 → Application 层
• Access Token Issued = 协议事实 → Api 层

这体现的是一种语义上的一致性,而不是形式上的对称。

这一原则带来的长期收益

将 Login 计数放在 Application 层,会带来一系列非常“工程化”的长期收益:
• 所有登录方式(IdentityWeb、OAuth、未来的设备流等)都会被自动覆盖
• 不会因 Api 路径调整而漏算或重复计算
• Billing、MAU、审计等统计语义更加稳定、可解释
• Application 层自然成为业务事实的唯一真源(Single Source of Truth)

这个例子表面上是在讨论:“登录计数逻辑应该放在哪里?”但本质上,它回答的是一个更重要的问题:“谁有权定义一个业务事实已经发生?”在一个健康、可演进的分层架构中,答案始终是:Application 层。

Api 层负责把请求送到正确的地方,而 Application 层负责决定:世界发生了什么变化。

一旦确立了这一原则,许多原本模糊、纠结的架构选择,都会自然变得清晰而坚定。

From a “Login Counting” Question to Principles and Practical Techniques of Layered Architecture Design

Geoffrey Chen
December 16, 2025

When designing and developing systems with a layered architecture, we often encounter issues that appear subtle yet are highly prone to confusion. On the surface, these issues usually take the form of questions such as “Which layer should this code belong to?” But upon deeper reflection, we often find that they actually touch on the semantic boundaries between architectural layers.

During the development of Smallsoft Identity (SI), we encountered such a seemingly simple but highly instructive question:

After a user successfully logs in, where should the login count (Login Usage) be recorded?
In the Api layer, or in the Application layer?

At first glance, this looks like a mere implementation detail.
However, after careful discussion, it turns out to reveal a core design principle of layered architecture that is frequently overlooked.

The Superficially “Reasonable” Choice: Counting Logins in the Api Layer

From a code-structure perspective, counting logins in the Api layer feels like a very natural idea:

var result = await _login.ExecuteAsync(request);

if (result.Success)
{
// Count one login here?
}

The Api layer receives an HTTP request, invokes the LoginUserCommand in the Application layer, and receives a result with Success = true.

It is easy to arrive at the intuitive conclusion:

“The login succeeded, so let’s count the login here.”

In fact, this approach is quite common in many systems.

But the problem is this — architecturally, this approach is semantically incorrect.

The Key Point of Divergence: Api Knows “Success,” but Not “What Happened”

The only fact the Api layer truly knows is:

“A request returned successfully.”

However, the Api layer neither knows nor should attempt to judge the following critical business-semantic facts:
• Whether a new authenticated session was created
• Whether an existing session was reused
• Whether the login was silent or automatic
• Whether the internal implementation of LoginUserCommand might change in the future

In other words:

The Api layer observes “control-flow success,”
not “business-state transition.”

And login itself is not a control-flow concept—it is a business state change.

Where Does a “Login” Actually Occur?

In the SI implementation, a login truly occurs at this moment:

await _sessions.CreateSessionAsync(new Session { … });

The business meaning expressed by this line is very clear:

“A new, authenticated user session has been created.”

This is a definite, unambiguous, and irreversible business fact.

And this fact exists only within the use-case logic of the Application layer, not in the Api layer.

Therefore, a key conclusion naturally emerges:

Login is not an Api event; it is an Application event.

A Reusable Layering Principle

From this, we can distill a highly practical layering principle that can be applied repeatedly across many systems:

If an action represents the establishment of a business fact,
it should be confirmed and recorded in the Application layer;
the Api layer should only be responsible for request forwarding and result delivery,
and should not possess authority over business semantics.

The inverse of this principle is equally clear:
• Api layer: suitable for protocol-level, transport-level, and presentation-level events
• Application layer: suitable for business completion, state transitions, and facts becoming true

Why Access Token Counting Can Reside in the Api Layer

In the same system, we do choose to count Access Token issuance in the OAuth Controller (Api layer).

This is not a contradiction.

The reasons are:
• An Access Token is a direct product of the OAuth protocol
• The fact that a token is “issued” only becomes true within the OAuth Controller
• There is currently no protocol-independent IssueAccessToken business use case

Therefore:
• Login = business fact → Application layer
• Access Token Issued = protocol fact → Api layer

This reflects semantic consistency, rather than formal symmetry.

Long-Term Benefits of This Principle

Placing login counting in the Application layer brings a series of highly practical, engineering-oriented long-term benefits:
• All login methods (IdentityWeb, OAuth, future device flows, etc.) are automatically covered
• No missed counts or double counting caused by Api route changes
• Billing, MAU, and audit semantics become more stable and explainable
• The Application layer naturally becomes the Single Source of Truth for business facts

On the surface, this example discusses the question:

“Where should login counting logic be placed?”

At a deeper level, it answers a far more important question:

“Who has the authority to define that a business fact has occurred?”

In a healthy, evolvable layered architecture, the answer is always:

The Application layer.

The Api layer is responsible for delivering requests to the correct place,
while the Application layer is responsible for deciding what has actually changed in the world.

Once this principle is firmly established, many architectural decisions that once felt ambiguous or conflicted naturally become clear and decisive.


Contact: Geoffrey Chen [email protected]

Verified GPG Public Key: https://keys.openpgp.org/[email protected]


Discover more from Geoffrey Chen

Subscribe to get the latest posts sent to your email.

Leave a Reply