In today's frontend world, we're constantly building more complex applications where business logic, UI components, and framework-specific code get tangled together, making our systems rigid and hard to maintain. I love how Robert C. Martin puts it: "Software was invented to be 'soft.' It was intended to be a way to easily change the behavior of machines. If we'd wanted the behavior of machines to be hard to change, we would have called it hardware." Let's explore how Clean Architecture principles can actually work in modern frontend development, with real patterns and practices that lead to maintainable, testable, and framework-independent applications.The Reality of Technical Debt and Market Pressure We've all heard this one before: "We can clean it up later; we just have to get to market first!" But let's be real - things never get cleaned up later because market pressures don't just disappear. "The fact is that making messes is always slower than staying clean, no matter which time scale you're using."
The Reality of Technical Debt and Market Pressure
"These developers buy into a familiar lie: 'We can clean it up later; we just have to get to market first!' Of course, things never do get cleaned up later because market pressures never abate."
"The fact is that making messes is always slower than staying clean, no matter which time scale you are using."
Clean Architecture Boundaries At every architectural boundary, you'll likely find the Humble Object pattern lurking nearby. Here's how that looks in practice:
// ❌ Quick and Dirty Implementation
const ProductPage: React.FC = () => {
const [product, setProduct] = useState<Product | null>(null);
const [relatedProducts, setRelatedProducts] = useState<Product[]>([]);
const [cart, setCart] = useState<Cart | null>(null);
const [error, setError] = useState<string | null>(null);
// Messy side effects
useEffect(() => {
fetch(`/api/products/${id}`)
.then(res => res.json())
.then(setProduct)
.catch(err => setError(err.message));
}, [id]);
// Business logic mixed with UI logic
const addToCart = async () => {
try {
const response = await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId: product?.id }),
});
const updatedCart = await response.json();
setCart(updatedCart);
// Direct DOM manipulation
window.alert('Added to cart!');
} catch (err) {
setError((err as Error).message);
}
};
};
// ✅ Clean, Modular Implementation
// Domain Entity
interface Product {
id: string;
name: string;
price: Money;
inventory: Inventory;
}
// Use Case / Application Service
class ProductService {
constructor(private readonly productRepo: ProductRepository) {}
async getProduct(id: string): Promise<Product> {
return this.productRepo.findById(id);
}
async getRelatedProducts(product: Product): Promise<Product[]> {
return this.productRepo.findRelated(product);
}
}
// Repository Interface
interface ProductRepository {
findById(id: string): Promise<Product>;
findRelated(product: Product): Promise<Product[]>;
}
// Custom Hook (Framework-specific adapter)
const useProduct = (id: string) => {
return useQuery(['product', id], () => productService.getProduct(id));
};
// Clean Component
const ProductPage: React.FC = () => {
const { data: product, isLoading, error } = useProduct(id);
const { data: relatedProducts } = useRelatedProducts(product);
const { addToCart, isAdding } = useCartMutation();
if (isLoading) return <ProductSkeleton />;
if (error) return <ErrorBoundary error={error} />;
return (
<ProductLayout>
<ProductDetails product={product} />
<AddToCartButton
onAdd={() => addToCart(product.id)}
isLoading={isAdding}
/>
<RelatedProducts products={relatedProducts} />
</ProductLayout>
);
};
Architecture and Framework Independence
"The Web is a delivery mechanism... and your architecture should treat it as such."
"If the framework wants you to derive your business objects from its base classes, say no! Derive proxies instead, and keep those proxies in components that are plugins to your business rules."
// Domain Entities (Framework Independent)
interface Order {
id: string;
items: OrderItem[];
total: Money;
status: OrderStatus;
}
// Business Rules (Pure TypeScript)
class OrderCalculator {
calculateTotal(items: OrderItem[]): Money {
return items.reduce(
(total, item) => total.add(item.price.multiply(item.quantity)),
Money.zero()
);
}
applyDiscount(order: Order, discount: Discount): Order {
return {
...order,
total: order.total.subtract(this.calculateDiscount(order, discount))
};
}
}
// Framework Adapter (React-specific)
const useOrder = (orderId: string) => {
const calculator = new OrderCalculator();
const { data: order } = useQuery(['order', orderId], fetchOrder);
return {
order,
applyDiscount: (discount: Discount) =>
calculator.applyDiscount(order!, discount),
// other order operations...
};
};
// UI Component (Framework-specific)
const OrderSummary: React.FC<{ orderId: string }> = ({ orderId }) => {
const { order, applyDiscount } = useOrder(orderId);
// React-specific rendering logic
};
Clean Architecture Boundaries
"At each architectural boundary, we are likely to find the Humble Object pattern lurking somewhere nearby."
// Domain Layer (Business Rules)
interface PaymentProcessor {
process(payment: Payment): Promise<PaymentResult>;
}
// Application Layer (Use Cases)
class PaymentService {
constructor(private processor: PaymentProcessor) {}
async processPayment(order: Order): Promise<PaymentResult> {
// Business logic for payment processing
return this.processor.process({
amount: order.total,
currency: order.currency,
orderId: order.id
});
}
}
// Infrastructure Layer (External Services)
class StripePaymentProcessor implements PaymentProcessor {
constructor(private stripe: Stripe) {}
async process(payment: Payment): Promise<PaymentResult> {
const result = await this.stripe.charges.create({
amount: payment.amount.value,
currency: payment.currency,
metadata: { orderId: payment.orderId }
});
return this.mapToPaymentResult(result);
}
}
// Presentation Layer (React Components)
const CheckoutButton: React.FC<{ order: Order }> = ({ order }) => {
const paymentService = usePaymentService();
const mutation = useMutation(
() => paymentService.processPayment(order),
{
onSuccess: (result) => {
// Handle success
},
onError: (error) => {
// Handle error
}
}
);
return (
<Button
onClick={() => mutation.mutate()}
loading={mutation.isLoading}
>
Pay Now
</Button>
);
};
Testing and Dependency Management
"Don't depend on things you don't need."
Testing and Dependency Management Let's talk about dependencies - specifically, why you shouldn't depend on things you don't need.
// ❌ Hard to Test Component with Direct Dependencies
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User>();
useEffect(() => {
axios.get('/api/user').then(res => setUser(res.data));
}, []);
return <div>{user?.name}</div>;
};
// ✅ Testable Component with Injected Dependencies
interface UserProfileProps {
userService: UserService;
}
const UserProfile: React.FC<UserProfileProps> = ({ userService }) => {
const { data: user } = useQuery(
'user',
() => userService.getCurrentUser()
);
return <div>{user?.name}</div>;
};
// Easy to test with mock service
describe('UserProfile', () => {
it('displays user name', async () => {
const mockService = {
getCurrentUser: jest.fn().mockResolvedValue({ name: 'John' })
};
render(<UserProfile userService={mockService} />);
expect(await screen.findByText('John')).toBeInTheDocument();
});
});
Conclusion
"Good architects are careful to separate details from policy, and then decouple the policy from the details so thoroughly that the policy has no knowledge of the details."
Conclusion Great architects know how to separate details from policy, and then decouple them so thoroughly that the policy has no clue about the details. This guide shows you how to:
- Keep your code framework-independent
- Make testing a breeze
- Handle dependencies like a pro
- Separate concerns properly
- Build applications that can actually scale
Remember, these aren't rigid rules - they're tools. Use them when they make sense for your project.Want to dive deeper into any of these topics? Just let me know!
This article created with the help of AI using my notes from the Clean Architecture book
Top comments (1)
Thnx for the article. ❤️