Clean Architecture in Front-End Development: Principles, Practices, and Comparative Analysis
The article explains why solid software architecture matters, introduces Robert C. Martin’s Clean Architecture and its framework‑, UI‑, database‑, and service‑independent principles, compares it with Hexagonal and DDD layered styles, details strategic and tactical phases for front‑end MVVM projects, provides layered code samples, and weighs its benefits against the initial overhead, urging teams to choose the approach that fits their project’s complexity and lifespan.
This article introduces the concept of software architecture and explains why a well‑designed architecture is essential for reducing complexity, maintenance cost, and improving productivity.
It defines architecture as the means to control system complexity, aiming to satisfy functional requirements with minimal human and operational cost.
The article then asks what constitutes a good architecture and proposes that a good architecture should be low‑cost to modify, easy to understand, and maintainable throughout its lifecycle.
Clean Architecture is presented as a classic architectural style proposed by Robert C. Martin ("Uncle Bob"). Its main characteristics are:
Framework‑independent: Business logic should not depend on any specific UI framework or library.
Testable: Business logic can be tested without UI, database, or server dependencies.
UI‑independent: Business logic is not tightly coupled with the UI.
Database‑independent: Switching databases (e.g., MySQL ↔ MongoDB) does not affect core logic.
External‑service‑independent: Changes in external services do not impact core business logic.
The article compares Clean Architecture with Hexagonal Architecture and DDD Layered Architecture, highlighting their common focus on domain‑centric design and separation of concerns.
It outlines a strategic phase (business analysis, domain modeling) and a tactical phase (layered implementation). The strategic phase includes:
Identifying business participants (actors).
Analyzing actions and resulting states for each participant.
Extracting domain objects (entities, value objects, aggregate roots).
Defining module boundaries based on aggregates.
In the tactical phase, the article demonstrates how to split a typical front‑end MVVM project into Clean Architecture layers:
Entity Layer: Contains domain entities with business rules (e.g., User, Product).
Use‑Case Layer: Coordinates entities to implement application scenarios.
Adapter Layer: Implements interfaces to external services and frameworks.
Framework & Driver Layer: UI frameworks, state management, and third‑party libraries.
Code examples are provided to illustrate each layer.
// User entity (./shared/domain/entities/user.ts)
import cookie from 'cookie';
export interface IUserService {
getCity(id: string): Promise
;
}
export class User {
public id: string;
private userService: IUserService;
constructor(id: string, userService: IUserService) {}
public isLogin(): boolean {
if (cookie.get('openid') && cookie.get('access_token')) {
return true;
}
return false;
}
public login(): Promise
{
if (!this.isLogin()) {
goToURL('https://www.xxx.com/login');
}
}
public logout(): Promise
{
cookie.remove('openid');
cookie.remove('access_token');
goToURL('https://www.xxx.com/login');
}
public getCity(): Promise
{
return this.userService.getCity(this.id);
}
} // Product entity (./shared/domain/entities/product.ts)
export interface IProductService {
getBaseInfoById(id: string): Promise
;
getStockInfoByIdAndCity(id: string, city: City): Promise
;
}
export class Product {
public id: string;
private productService: IProductService;
constructor(id: string, productService: IProductService) {}
public async getDetail() {
const baseInfo = await this.productService.getBaseInfoById(this.id);
const stockInfo = await this.productService.getStockInfoById(this.id, city);
const detail = {
id: this.id,
name: baseInfo.name,
images: baseInfo.images,
stockNum: stockInfo.num,
};
return detail;
}
public addToCart(num: number) {
return this.productService.getStockInfoById(this.id, city);
}
} // Use‑case: get product detail (./shared/domain/usercases/get-product-detail.ts)
import { User } from './shared/domain/entities/user.ts';
import { Product } from './shared/domain/entities/product.ts';
import { UserService } from './server/services/user-service.ts';
import { ProductService } from './server/services/product-service.ts';
export async function getProductDetail(userId: string, productId: string) {
const user = new User(userId, new UserService());
const product = new Product(productId, new ProductService());
const city: City = await user.getCity();
const productBaseInfo = await product.getBaseInfo();
const productStockInfo = await product.getStockInfo(city);
return {
baseInfo: productBaseInfo,
stockInfo: productStockInfo,
};
} // User service implementation (./server/services/user-service.ts)
import { IUserService } from '../shared/domain/entities/user.ts';
class UserService implements IUserService {
async getCity(userId: string): Promise
{
const resp = get('https://api.xxx.com/queryUserCity', { userId });
if (resp.ret !== 0) {
throw new Error('查询用户所在城市失败');
}
return resp.data.city as City;
}
}
export { UserService }; // Product service implementation (./server/services/product-service.ts)
import { IProductService } from '../shared/domain/entities/product.ts';
class ProductService implements IProductService {
async getBaseInfoById(id: string): Promise
{
// call backend API
}
async getStockInfoByIdAndCity(id: string, city: City): Promise
{
// call backend API
}
}
export { ProductService }; // Vuex store for product detail (./client/store/product-store.ts)
import { getProductDetail } from '../shared/domain/usercases/get-product-detail.ts';
export default new Vuex.Store({
state: {
productDetail: null,
},
mutations: {
async fetchProductDetail(state, { userId, productId }) {
state.productDetail = await getProductDetail(userId, productId);
},
},
}); // Product detail page component (./client/pages/product-detail.ts)
import { defineComponent, onMounted } from 'vue';
import store from '../store/product-store';
export default defineComponent({
name: 'ProductDetailPage',
setup() {
onMounted(async () => {
setLoading(true);
await store.dispatch('fetchProductDetail', { userId, productId });
setLoading(false);
});
return () => (
{store.state.productDetail.baseInfo}
{store.state.productDetail.stockInfo}
);
},
});The article also discusses the pros and cons of applying Clean Architecture to front‑end projects. Advantages include clean separation of business logic, easier testing, and flexibility to switch UI frameworks. Disadvantages involve higher initial overhead for defining ports and adapters, which may reduce development speed for simple or short‑lived projects.
Finally, the author emphasizes that there is no "silver bullet" architecture; teams should choose the style that fits their project complexity and lifecycle. For long‑term, complex front‑end systems (e.g., Tencent Docs, low‑code engines), Clean Architecture can significantly lower maintenance costs and facilitate framework upgrades.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.