I Will Reject Your Pull Request If You Violate These Design Principles
4 Principles for Clean Code That Won’t Get Your PR Rejected

After reviewing hundreds of PRs and rejecting a good bunch, I went hunting for universal rules to prevent code complexity.
Like most developers, this has been a learning journey — I’ve written my share of messy modules too. But understanding core design principles could have saved me from costly mistakes.
Here’s the fundamental truth: Good software design minimizes complexity. But to fight complexity, we first need to understand our enemy.
Complexity: The Enemy of Software Systems
To design clean modules, we first need to understand complexity’s nature.
Complexity doesn’t emerge from a single bad decision. Instead, it grows gradually over time — like a slow-spreading disease — unless we actively fight against it.
In software development, complexity is a natural byproduct that accumulates when left unchecked. Just as gardeners regularly prune plants to prevent overgrowth, we must continually refactor and simplify our code to keep complexity at bay.
Here’s a useful definition from A Philosophy of Software Design:
A system’s complexity equals the sum of its parts’ complexities, weighted by how often developers interact with them.
In simpler terms: the code we touch most contributes most to our complexity burden
The complexity of a system is the sum of complexities of its individual parts, weighted by how frequently those parts are interacted with by developers. In other words, parts of a system that are worked on more often contribute more to the overall complexity.
This leads to a key insight:
Isolating complexity in rarely-visited code is nearly as effective as eliminating it entirely.
Go For Deep Modules And Hide Complexity
When designing modules, focus on two core objectives:
Contain complexity within the module
Create interfaces that are intuitive for other developers
Here’s how to achieve this through deep module design:
Hide complexity: A well-designed module acts like a black box — messy details stay hidden behind a simple interface. The more complexity it conceals, the more valuable it becomes.
Aim for Depth: Deep modules act like icebergs — what you see (the interface) is small and simple, while the powerful functionality remains hidden beneath the surface. They maximize capability while minimizing cognitive load for users.
Avoid Shallow Modules: Shallow modules expose too many implementation details, forcing users to understand internal workings and increasing system complexity.
Minimize Dependencies: By isolating implementation details, deep modules allow internal changes without system-wide impacts, dramatically reducing maintenance costs.
Invest in Design: While creating deep modules requires more upfront effort, the long-term payoff in maintainability and scalability makes it worthwhile.
This approach overturns a common misconception: smaller modules aren’t inherently better. Instead, focus on modules that effectively hide complexity while delivering robust functionality.
During my three years at Cresta, I faced a perfect storm of complexity:
Limited engineering resources forced us to outsource a critical frontend project
The external team built a complex multi-layer drag-and-drop form
When we brought development in-house, I inherited the cleanup effort
(Let’s just say I spent a significant portion of 2024 refactoring that codebase 🙃)
This experience became a masterclass in what not to do. Before we analyze the design flaws, here’s a simplified view of what we were building:
Here’s how the system worked:
Block Combination: Users could combine different blocks using logical operators (AND/OR)
Block Management: Clicking the blue “+” button revealed a UI for: Selecting existing blocks, or creating new blocks
Block Composition: Each block contained a set of input fields for user data
Relationship Control: Clicking yellow squares allowed users to modify logical relationships between blocks.
🛑 Bad (shallow) design
When you inspect the code above you should notice following problems:
It exposes too many implementation details (shallow modules)
The main component handles too many responsibilities
State management is scattered and tightly coupled
Position logic is leaked to multiple components
Relationship handling is mixed with UI logic
No clear separation of concerns
Direct manipulation of complex state structures
No abstraction of the underlying data model
Business logic mixed with presentation logic
Let’s investigate now how we could design this better.
✅ Good design:
The code is longer, however, it is also way better desgined than the previous implementation. Now you may ask, why is it better?
Deep Modules:
Each component and hook has a focused responsibility with a simple interface but complex internal implementation.
Hidden Complexity:
Complex state management is hidden in `useBlockChain`
Block creation logic is encapsulated in `BlockFactory`
Chain operations are isolated in `ChainOperations`
Minimal Dependencies:
Components only depend on their immediate needs
Business logic is separated from UI components
State management is centralized and predictable
Clear Interfaces:
Components expose only what’s necessary
State updates are handled through well-defined actions
Complex operations are hidden behind simple method calls
Separation of Concerns:
UI components focus on rendering
Business logic is in separate services
State management is handled by specialized hooks
Maintainable Structure:
Each piece is independently testable
Changes can be made without affecting other parts
New features can be added by extending existing patterns
I Design According to These 4 Principles
When approaching new features, I follow these core principles:
I need to hide information: At the core of good design is information hiding. I need to encapsulate implementation details within modules so that changes to a module’s internals won’t ripple through the system.
I need to avoid unnecessary dependencies: When implementation details are exposed, changes create tight coupling across the codebase. I need to ensure modules hide their internal state and logic to make future modifications easier.
I need to centralize and hide complexity: Complexity should live inside modules, not spread across the system. I need to push complexity inward, exposing only simple interfaces to reduce cognitive load for other developers.
I need to enable independent evolution: With hidden internals, modules can evolve independently. I need to design modules so changes in one area don’t break others, reducing maintenance costs and bugs.
When I approach a UI design, my first question is always: “What modules will interact here?”. This mindset naturally applies the four principles we discussed.
Take a “User Management” UI as an example. My first step is identifying the key modules and their interactions:
In this case I can think of following high-level components that will interact with each other:
UserManagement: Manages state of visible users
FilterBar: Handles filter state (e.g., search queries)
AddUser: Button triggering a user creation modal
UserTable: Displays and manages the user list
UserRow: Represents individual users, Includes action buttons in a popover
To design effectively, I need a clear feature description. My options:
Write it myself
Use the product manager’s PRD
Leverage AI (o1 outperforms 4o for this task)
With a description in hand, I turn to Claude for a blueprint
Hi Claude,
I hope you're having an amazing day! I need your help designing a React feature.
Here's the feature description:
<description>
Please incorporate these components:
<list of components>
Create a high-level blueprint focusing on:
- State flow
- Data flow
- Ideal-world architecture
Claude’s output provides a clean, modular design that aligns with our principles.
While uploading UI screenshots to Claude can generate quick blueprints, complex B2B applications often demand deeper consideration. In these cases, a detailed feature description and thoughtful upfront design are important for creating maintainable, scalable solutions.
Recently, I’ve found great value in asking Claude (via Cursor) this specific question:
Take a look at @file_1, @file_2, @file_3.
In an ideal world scenario, how would you design this feature?
This approach helps me:
Evaluate existing feature designs
Identify improvement opportunities
Deepen my understanding of clean design principles
💡 Want More Tools to Help You Grow?
I love sharing tools and insights that help others grow — both as engineers and as humans.
If you’re enjoying this post, here are two things you shouldn’t miss:
🔧 Medium Insights — A tool I built to explore the analytics behind any Medium author, helping you uncover what works and why.
📚 Everything I Learned About Life — A Notion doc where I reflect on lessons learned, practical tools I use daily, and content that keeps me growing.
👉 Find both (and more) here: Start Here