Skip to main content

KISS (Keep It Simple, Stupid)

The KISS Principle

It's like following a recipe with 5 ingredients versus one with 25. The simpler approach is usually easier to understand, has fewer things that can go wrong, and is quicker to prepare.

What is KISS?​

KISS, which stands for "Keep It Simple, Stupid," is a design principle that advocates for simplicity over complexity. The principle suggests that most systems work best when they are kept simple rather than made complex. Simplicity should be a key goal in design, and unnecessary complexity should be avoided.

Originally attributed to Kelly Johnson, a lead engineer at Lockheed Skunk Works, the principle has become a fundamental concept in software development. The essence of KISS is that simple solutions are more reliable, easier to understand, and ultimately more successful than complex ones.

Why KISS Matters​

Improved Maintainability​

Simple code is easier to understand, debug, and modify. When developers (including your future self) revisit code, simple designs allow them to quickly grasp the functionality and make changes with confidence.

Reduced Bugs​

Complex systems have more potential points of failure. By keeping things simple, you reduce the surface area for bugs and make it easier to identify and fix issues when they occur.

Enhanced Collaboration​

Simple code enables better collaboration among team members. When code is straightforward, onboarding new developers is faster, and knowledge sharing becomes more effective.

Better Performance​

Simple solutions often perform better than complex ones. They typically have fewer components, less overhead, and clearer execution paths.

Faster Development​

Building simple solutions usually takes less time than implementing complex ones, allowing for quicker iteration and delivery.

KISS in Practice​

Example 1: Simple Versus Complex Function​

Consider a function that calculates the average of numbers in an array:

Calculating Average

Comparing overly complex solution with a simple, focused approach

Complex ApproachAvoid
// Complex approach - overly abstract and handles too many cases
function calculateStatistic(numbers, type = 'average', options = {}) {
if (!Array.isArray(numbers)) {
if (typeof numbers === 'number') {
numbers = [numbers];
} else {
throw new Error('Input must be an array or number');
}
}

if (numbers.length === 0) {
return options.emptyResult || 0;
}

// Filter out non-numeric values
const validNumbers = options.strictMode
? numbers.filter(n => typeof n === 'number' && !isNaN(n))
: numbers.map(n => parseFloat(n)).filter(n => !isNaN(n));

if (validNumbers.length === 0) {
return options.emptyResult || 0;
}

switch (type.toLowerCase()) {
case 'average':
case 'mean':
return validNumbers.reduce((sum, num) => sum + num, 0) / validNumbers.length;
case 'median': {
const sorted = [...validNumbers].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[middle - 1] + sorted[middle]) / 2
: sorted[middle];
}
case 'mode': {
const counts = validNumbers.reduce((acc, num) => {
acc[num] = (acc[num] || 0) + 1;
return acc;
}, {});

let maxCount = 0;
let mode = null;

for (const [num, count] of Object.entries(counts)) {
if (count > maxCount) {
maxCount = count;
mode = parseFloat(num);
}
}

return mode;
}
default:
throw new Error(`Unsupported statistic type: ${type}`);
}
}
KISS ApproachRecommended
// Simple approach - does one thing well
function calculateAverage(numbers) {
if (numbers.length === 0) {
return 0;
}

let sum = 0;
for (const number of numbers) {
sum += number;
}

return sum / numbers.length;
}

The simple approach:

  1. Has a clear, specific purpose
  2. Is easier to understand and test
  3. Has fewer potential points of failure
  4. Doesn't try to handle every possible scenario

If other statistics are needed, they can be implemented as separate, focused functions:

function calculateMedian(numbers) {
if (numbers.length === 0) return 0;

const sorted = [...numbers].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);

return sorted.length % 2 === 0
? (sorted[middle - 1] + sorted[middle]) / 2
: sorted[middle];
}

function calculateMode(numbers) {
if (numbers.length === 0) return null;

const counts = {};
let maxCount = 0;
let mode = null;

for (const num of numbers) {
counts[num] = (counts[num] || 0) + 1;
if (counts[num] > maxCount) {
maxCount = counts[num];
mode = num;
}
}

return mode;
}

Example 2: Simple Versus Complex Architecture​

Consider a task management application where users can create and organize tasks.

Task Management System

Comparing an overly complex architecture with a simpler approach

Over-engineeredAvoid
// Overly complex architecture with excessive abstraction
class BaseEntity {
constructor(id) {
this.id = id;
this.createdAt = new Date();
this.updatedAt = new Date();
}

update() {
this.updatedAt = new Date();
}
}

class Task extends BaseEntity {
constructor(id, title, description = '') {
super(id);
this.title = title;
this.description = description;
this.status = 'pending';
this.priority = 'medium';
this.tags = [];
this.attachments = [];
this.comments = [];
this.assignee = null;
}

setStatus(status) {
const validStatuses = ['pending', 'in_progress', 'completed', 'cancelled'];
if (!validStatuses.includes(status)) {
throw new Error(`Invalid status: ${status}`);
}
this.status = status;
this.update();
}

setPriority(priority) {
const validPriorities = ['low', 'medium', 'high', 'urgent'];
if (!validPriorities.includes(priority)) {
throw new Error(`Invalid priority: ${priority}`);
}
this.priority = priority;
this.update();
}

addTag(tag) {
if (!this.tags.includes(tag)) {
this.tags.push(tag);
this.update();
}
}

removeTag(tag) {
const index = this.tags.indexOf(tag);
if (index !== -1) {
this.tags.splice(index, 1);
this.update();
}
}

addAttachment(attachment) {
this.attachments.push(attachment);
this.update();
}

removeAttachment(attachmentId) {
const index = this.attachments.findIndex(a => a.id === attachmentId);
if (index !== -1) {
this.attachments.splice(index, 1);
this.update();
}
}

addComment(comment) {
this.comments.push(comment);
this.update();
}

assignTo(userId) {
this.assignee = userId;
this.update();
}
}

class TaskRepository {
constructor(storageProvider) {
this.storageProvider = storageProvider;
}

async findById(id) {
const data = await this.storageProvider.get(`tasks/${id}`);
if (!data) return null;

const task = new Task(id, data.title, data.description);
Object.assign(task, data);
return task;
}

async save(task) {
task.update();
await this.storageProvider.set(`tasks/${task.id}`, task);
return task;
}

async delete(id) {
await this.storageProvider.delete(`tasks/${id}`);
}

async findAll() {
const tasksData = await this.storageProvider.query('tasks');
return tasksData.map(data => {
const task = new Task(data.id, data.title, data.description);
Object.assign(task, data);
return task;
});
}

async findByStatus(status) {
const tasks = await this.findAll();
return tasks.filter(task => task.status === status);
}

async findByAssignee(userId) {
const tasks = await this.findAll();
return tasks.filter(task => task.assignee === userId);
}
}

class StorageProvider {
constructor() {
throw new Error('StorageProvider is an abstract class');
}

async get(key) {
throw new Error('Not implemented');
}

async set(key, value) {
throw new Error('Not implemented');
}

async delete(key) {
throw new Error('Not implemented');
}

async query(prefix) {
throw new Error('Not implemented');
}
}

class LocalStorageProvider extends StorageProvider {
async get(key) {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
}

async set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}

async delete(key) {
localStorage.removeItem(key);
}

async query(prefix) {
const results = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(prefix)) {
results.push(JSON.parse(localStorage.getItem(key)));
}
}
return results;
}
}

class TaskService {
constructor(taskRepository) {
this.taskRepository = taskRepository;
}

async createTask(title, description) {
const id = Date.now().toString();
const task = new Task(id, title, description);
await this.taskRepository.save(task);
return task;
}

async updateTask(id, updates) {
const task = await this.taskRepository.findById(id);
if (!task) {
throw new Error(`Task not found: ${id}`);
}

Object.assign(task, updates);
await this.taskRepository.save(task);
return task;
}

async deleteTask(id) {
await this.taskRepository.delete(id);
}

async getTaskById(id) {
return await this.taskRepository.findById(id);
}

async getAllTasks() {
return await this.taskRepository.findAll();
}

async getTasksByStatus(status) {
return await this.taskRepository.findByStatus(status);
}

async getTasksByAssignee(userId) {
return await this.taskRepository.findByAssignee(userId);
}
}

// Usage
const storageProvider = new LocalStorageProvider();
const taskRepository = new TaskRepository(storageProvider);
const taskService = new TaskService(taskRepository);

// Create a task
taskService.createTask('Learn KISS', 'Study the Keep It Simple principle')
.then(task => console.log('Task created:', task));
KISS ApproachRecommended
// Simple task management system
const TaskManager = {
// Create a new task
createTask(title, description = '') {
const task = {
id: Date.now().toString(),
title,
description,
status: 'pending',
createdAt: new Date()
};

// Store the task
const tasks = this.getAllTasks();
tasks.push(task);
localStorage.setItem('tasks', JSON.stringify(tasks));

return task;
},

// Get a task by ID
getTask(id) {
const tasks = this.getAllTasks();
return tasks.find(task => task.id === id) || null;
},

// Update a task
updateTask(id, updates) {
const tasks = this.getAllTasks();
const index = tasks.findIndex(task => task.id === id);

if (index === -1) {
return null;
}

const updatedTask = { ...tasks[index], ...updates };
tasks[index] = updatedTask;
localStorage.setItem('tasks', JSON.stringify(tasks));

return updatedTask;
},

// Delete a task
deleteTask(id) {
const tasks = this.getAllTasks();
const filteredTasks = tasks.filter(task => task.id !== id);
localStorage.setItem('tasks', JSON.stringify(filteredTasks));
},

// Get all tasks
getAllTasks() {
const tasksJson = localStorage.getItem('tasks') || '[]';
return JSON.parse(tasksJson);
},

// Get tasks by status
getTasksByStatus(status) {
const tasks = this.getAllTasks();
return tasks.filter(task => task.status === status);
}
};

// Usage
const task = TaskManager.createTask('Learn KISS', 'Study the Keep It Simple principle');
console.log('Task created:', task);

// Update the task
TaskManager.updateTask(task.id, { status: 'completed' });

// Get all tasks
const allTasks = TaskManager.getAllTasks();
console.log('All tasks:', allTasks);

The simple approach:

  1. Uses a straightforward object with methods instead of complex class hierarchies
  2. Has fewer abstractions and indirections
  3. Is easier to understand and maintain
  4. Provides the core functionality without unnecessary complexity

Example 3: Simple Versus Complex UI Component​

Consider a button component in a UI library:

Button Component

Comparing an overly configurable component with a focused approach

Over-engineeredAvoid
// Overly complex button component
const Button = ({
children,
onClick,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon,
iconPosition = 'left',
fullWidth = false,
rounded = false,
elevation = 'medium',
animationStyle = 'ripple',
className,
style,
id,
ariaLabel,
dataTestId,
tooltipText,
tooltipPosition = 'top',
showTooltip = false,
...props
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [ripples, setRipples] = useState([]);

// Complex class generation
const buttonClasses = [
'button',
`button--${variant}`,
`button--${size}`,
fullWidth ? 'button--full-width' : '',
rounded ? 'button--rounded' : '',
`button--elevation-${elevation}`,
disabled ? 'button--disabled' : '',
loading ? 'button--loading' : '',
isHovered ? 'button--hovered' : '',
isFocused ? 'button--focused' : '',
isPressed ? 'button--pressed' : '',
className || '',
].filter(Boolean).join(' ');

// Complex event handling
const handleClick = (e) => {
if (disabled || loading) return;

// Create ripple effect
if (animationStyle === 'ripple') {
const rect = e.currentTarget.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;

const newRipple = {
id: Date.now(),
size,
x,
y,
};

setRipples([...ripples, newRipple]);

// Remove ripple after animation
setTimeout(() => {
setRipples(ripples => ripples.filter(r => r.id !== newRipple.id));
}, 1000);
}

if (onClick) onClick(e);
};

// Complex mouse event handling
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => {
setIsHovered(false);
setIsPressed(false);
};
const handleMouseDown = () => setIsPressed(true);
const handleMouseUp = () => setIsPressed(false);

// Complex focus event handling
const handleFocus = () => setIsFocused(true);
const handleBlur = () => setIsFocused(false);

// Complex rendering
return (
<div className="button-container" style={{ position: 'relative' }}>
{showTooltip && tooltipText && (
<div className={`tooltip tooltip--${tooltipPosition}`}>
{tooltipText}
</div>
)}

<button
id={id}
className={buttonClasses}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled || loading}
aria-label={ariaLabel}
data-testid={dataTestId}
style={style}
{...props}
>
{/* Ripple effects */}
{animationStyle === 'ripple' && ripples.map(ripple => (
<span
key={ripple.id}
className="button__ripple"
style={{
width: ripple.size,
height: ripple.size,
left: ripple.x,
top: ripple.y,
}}
/>
))}

{/* Loading spinner */}
{loading && (
<div className="button__spinner">
<div className="spinner" />
</div>
)}

{/* Button content */}
<div className="button__content">
{icon && iconPosition === 'left' && (
<span className="button__icon button__icon--left">{icon}</span>
)}

<span className="button__text">{children}</span>

{icon && iconPosition === 'right' && (
<span className="button__icon button__icon--right">{icon}</span>
)}
</div>
</button>
</div>
);
};
KISS ApproachRecommended
// Simple button component
const Button = ({
children,
onClick,
variant = 'primary',
size = 'medium',
disabled = false,
className,
...props
}) => {
// Simple class generation
const buttonClasses = [
'button',
`button--${variant}`,
`button--${size}`,
disabled ? 'button--disabled' : '',
className || '',
].filter(Boolean).join(' ');

return (
<button
className={buttonClasses}
onClick={onClick}
disabled={disabled}
{...props}
>
{children}
</button>
);
};

// Usage
function App() {
return (
<div>
<Button onClick={() => alert('Clicked!')}>Click Me</Button>
<Button variant="secondary" size="large">Large Button</Button>
<Button disabled>Disabled Button</Button>
</div>
);
}

The simple approach:

  1. Has fewer props and options
  2. Is easier to understand and use
  3. Has a simpler implementation
  4. Covers the most common use cases effectively

If additional functionality is needed, it can be added incrementally or through composition:

// Icon button through composition
const IconButton = ({ icon, children, ...props }) => (
<Button {...props}>
{icon && <span className="button__icon">{icon}</span>}
{children}
</Button>
);

// Loading button through composition
const LoadingButton = ({ loading, children, ...props }) => (
<Button {...props} disabled={props.disabled || loading}>
{loading ? <span className="spinner"></span> : children}
</Button>
);

Applying KISS in Your Code​

Do​

  • Start with the simplest solution: Implement the most straightforward approach first
  • Use built-in language features: Leverage the standard library before adding dependencies
  • Refactor for simplicity: Regularly review and simplify complex code
  • Choose clear names: Use descriptive variable and function names
  • Write short functions: Keep functions focused on a single responsibility

Don't​

  • Over-engineer: Avoid adding complexity for hypothetical scenarios
  • Prematurely optimize: Don't optimize code before you have evidence it's needed
  • Create unnecessary abstractions: Don't abstract until you have multiple concrete use cases
  • Add technologies for their own sake: Only include libraries that solve actual problems
  • Write clever code: Prioritize readability over cleverness

Identifying KISS Violations​

Signs that you might be violating KISS include:

  1. Long functions: Functions exceeding 25-30 lines might be doing too much
  2. Deep nesting: More than 2-3 levels of indentation indicates complexity
  3. Too many parameters: Functions with many parameters often do too much
  4. Excessive comments: Code that needs extensive comments might be too complex
  5. Multiple responsibilities: Classes or functions serving multiple purposes
  6. Unnecessary abstraction layers: Layers that don't provide clear value

Techniques for Simplification​

Decomposition​

Break complex functions or classes into smaller, focused units with clear responsibilities:

// Complex function
function processOrder(order) {
// 100 lines of code handling validation, calculation, saving, emails, etc.
}

// Decomposed into smaller, focused functions
function validateOrder(order) {
// 20 lines of validation logic
}

function calculateOrderTotals(order) {
// 25 lines of calculation logic
}

function saveOrderToDatabase(order) {
// 20 lines of database logic
}

function sendOrderConfirmation(order) {
// 35 lines of email logic
}

function processOrder(order) {
validateOrder(order);
calculateOrderTotals(order);
saveOrderToDatabase(order);
sendOrderConfirmation(order);
}

Elimination​

Remove features, code, or options that aren't essential to the current requirements:

// Before elimination
function fetchUserData(userId, options = {}) {
const {
includeProfile = true,
includeOrders = false,
includePayments = false,
includeFriends = false,
includeWishlist = false,
includeReviews = false,
includeActivity = false,
// Many more options...
} = options;

// Complex fetching logic based on all these options
}

// After elimination - focus on what's actually needed
function fetchUserBasicData(userId) {
// Fetch basic user data
}

function fetchUserOrders(userId) {
// Fetch user orders
}

// Other specific functions as needed

Standardization​

Use consistent patterns and approaches throughout the codebase:

// Inconsistent error handling
function fetchData1() {
try {
// Fetch data
} catch (e) {
console.log('Error:', e);
return null;
}
}

function fetchData2() {
// Fetch data
.then(data => data)
.catch(e => {
throw new Error(`Data fetch failed: ${e.message}`);
});
}

// Standardized error handling
function fetchData1() {
try {
// Fetch data
} catch (e) {
logger.error('Data fetch error', { error: e });
throw new ApiError('Failed to fetch data', { cause: e });
}
}

function fetchData2() {
try {
// Fetch data
} catch (e) {
logger.error('Data fetch error', { error: e });
throw new ApiError('Failed to fetch data', { cause: e });
}
}

Clarification​

Improve naming and structure to make the intent and functionality clearer:

// Unclear naming and structure
function p(d) {
const r = [];
for (let i = 0; i < d.length; i++) {
if (d[i].a > 0) {
r.push({
i: d[i].i,
v: d[i].a * 100
});
}
}
return r;
}

// Clear naming and structure
function calculateProductDiscounts(products) {
const discounts = [];

for (const product of products) {
if (product.discountRate > 0) {
discounts.push({
productId: product.id,
discountAmount: product.discountRate * 100
});
}
}

return discounts;
}

Balancing KISS with Extensibility​

While KISS encourages simplicity, software still needs to be maintainable and extensible. Balance is achieved by:

  1. Clean interfaces: Define clear, simple interfaces between components
  2. Separation of concerns: Keep distinct responsibilities in separate modules
  3. Focused refactoring: Reshape code when patterns emerge from concrete use cases
  4. Consistent patterns: Use similar approaches for similar problems

Real-World Examples of KISS​

Unix Philosophy​

The Unix philosophy embodies KISS principles:

  • Write programs that do one thing and do it well
  • Write programs to work together
  • Write programs to handle text streams, because that is a universal interface

React Hooks​

React Hooks simplified state management in React components:

// Class component (pre-hooks) - more complex
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}

increment() {
this.setState({ count: this.state.count + 1 });
}

render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

// Function component with hooks - simpler
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

JSON vs XML​

JSON's simplicity has contributed to its widespread adoption over XML for many use cases:

<!-- XML - more complex -->
<person>
<n>John Doe</n>
<age>30</age>
<email>john@example.com</email>
<addresses>
<address type="home">
<street>123 Main St</street>
<city>Anytown</city>
<state>CA</state>
<zip>12345</zip>
</address>
</addresses>
</person>
// JSON - simpler
{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"addresses": [
{
"type": "home",
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
}
]
}

Conclusion​

KISS is a powerful principle that helps create more maintainable, reliable software. By prioritizing simplicity, developers can build systems that are easier to understand, extend, and maintain.

Remember that simplicity doesn't mean sacrificing functionality or being simplistic. It means finding the most straightforward solution that effectively solves the problem without unnecessary complexity.

When faced with design decisions, ask yourself: "What's the simplest approach that could work?" Starting simple doesn't prevent you from adding complexity later if truly needed, but it does protect you from the burden of unnecessary complexity from the start.