Advertisement

Home/Coding & Tech Skills

Separation of Concerns Explained: 3 Beginner-Friendly Examples

coding-tech-skills · Coding & Tech Skills

Advertisement

I still remember the first time I opened a coworker's project and found a single file with 800 lines of HTML, CSS, and JavaScript all mashed together. Scrolling through it felt like untangling a bag of charging cables. That's the exact moment I understood why separation of concerns (SoC) isn't just some academic buzzword — it's the difference between code you can confidently tweak and code that makes you sweat every time you need to change a button color.

Advertisement

At its core, separation of concerns means breaking your code into distinct sections, each responsible for one specific job. Think of it like a restaurant kitchen: the chef cooks, the server takes orders, and the dishwasher handles dirty plates. If the chef also had to wash dishes between every entrée, nothing would get done on time. Same with code. When each piece does only its own thing, you can swap, fix, or upgrade parts without breaking everything else.

For beginners, the immediate payoff is clarity. When I taught myself Python, my early scripts were one big block of spaghetti. Adding a new feature meant reading the entire mess to find where to insert a line. After I started separating concerns — even just splitting input, processing, and output into functions — debugging went from a half-hour hunt to a two-minute check. SoC doesn't require fancy frameworks or design patterns; it's a mindset shift you can apply today.

Example 1: Splitting HTML, CSS, and JavaScript (The Web Dev Classic)

If you've ever written a web page, you've probably seen this pattern gone wrong. Here's the before — a single HTML file with inline styles and inline scripts mixed into the markup:

<!DOCTYPE html>
<html>
<body>
<h1 style="color: red; font-size: 24px;" onclick="alert('Hello!')">Welcome</h1>
<p style="margin: 10px;" onmouseover="this.style.color='blue'">Hover over me</p>
</body>
</html>

That works, but try changing the hover color across ten pages, and you'll be editing each onmouseover line individually. Now here's the same page with concerns separated:

index.html (structure only):

<!DOCTYPE html>
<html>
<head><link rel="stylesheet" href="styles.css"></head>
<body>
<h1 id="welcome">Welcome</h1>
<p id="hover-text">Hover over me</p>
<script src="script.js"></script>
</body>
</html>

styles.css (presentation only):

#welcome { color: red; font-size: 24px; }
#hover-text { margin: 10px; }
#hover-text:hover { color: blue; }

script.js (behavior only):

document.getElementById('welcome').addEventListener('click', function() {
  alert('Hello!');
});

Now changing the hover color is a one-line edit in the CSS file. Adding a new behavior (like a double-click) only touches the JavaScript. The HTML stays clean. When I first refactored my personal site this way, I cut page load time by 40% because the browser could cache the CSS and JS separately. Not bad for just moving some code around.

Example 2: Breaking a Python Script into Functions and Modules

Let me walk you through a Python script I wrote early in my coding journey. It processed a list of user sign-ups — read a CSV, cleaned names, and printed a summary. Here's the monolithic version:

import csv
with open('users.csv', 'r') as f:
    reader = csv.DictReader(f)
    names = []
    for row in reader:
        name = row['name'].strip().title()
        if name:
            names.append(name)
print(f"Total valid names: {len(names)}")
for n in names:
    print(n)

That's only 11 lines, but it's a single block. If I wanted to change the cleaning logic (say, also remove middle initials), I'd have to edit inside the loop that also reads the file and builds the list. Not a big deal for a tiny script, but when I later added email validation and a save-to-database step, that file ballooned to 150 lines and became a headache.

Here's the refactored version with separation of concerns:

# user_processing.py
import csv

def load_users(filepath):
    with open(filepath, 'r') as f:
        return list(csv.DictReader(f))

def clean_name(name):
    cleaned = name.strip().title()
    return cleaned if cleaned else None

def process_users(raw_data):
    users = []
    for row in raw_data:
        name = clean_name(row.get('name', ''))
        if name:
            users.append({'name': name})
    return users

def print_summary(users):
    print(f"Total valid names: {len(users)}")
    for u in users:
        print(u['name'])

main.py:

from user_processing import load_users, process_users, print_summary
raw = load_users('users.csv')
users = process_users(raw)
print_summary(users)

Now each function has one job. clean_name handles just the name cleaning. load_users only reads the file. print_summary only displays. I can test clean_name in isolation with a simple unit test. And if I later switch from CSV to a database, I only change load_users — the rest stays untouched. When I did this for a real project at work, my manager asked how I finished a feature in half the estimated time. The answer was SoC.

Example 3: Separating Data, Logic, and Presentation in a Simple App

Now let's stretch a bit. Imagine a to-do list app. Without separation, you might have a single script that stores tasks in a list, checks if they're overdue, and draws the HTML all at once. Here's a rough sketch of that mess:

tasks = [{'text': 'Buy milk', 'done': False}]
html = '<ul>'
for t in tasks:
    status = '✓' if t['done'] else '✗'
    html += f'<li>{status} {t["text"]}</li>'
html += '</ul>'
print(html)

That's tightly coupled. If you change the data structure (add a due date), you have to rewrite the loop and the HTML generation. Enter a simplified MVC-lite pattern:

model.py (data):

tasks = [{'text': 'Buy milk', 'done': False, 'due': '2025-03-01'}]
def add_task(text, due):
    tasks.append({'text': text, 'done': False, 'due': due})
def toggle_task(index):
    tasks[index]['done'] = not tasks[index]['done']

controller.py (logic):

from model import tasks, add_task, toggle_task
def get_overdue_tasks():
    from datetime import date
    return [t for t in tasks if not t['done'] and t['due']  < str(date.today())]

view.py (presentation):

from controller import get_overdue_tasks
def render_overdue():
    overdue = get_overdue_tasks()
    html = '<ul>'
    for t in overdue:
        html += f'<li>⚠ {t["text"]} (due {t["due"]})</li>'
    html += '</ul>'
    return html

Now the data lives in model.py, the overdue logic lives in controller.py, and the display lives in view.py. Want to change the warning icon from ⚠ to 🚩? Edit one line in view.py. Want to change overdue to also flag tasks due tomorrow? Change one function in controller.py. The first time I built an app this way, I realized I'd been fighting my own code for months. It was like finally cleaning a messy desk — everything suddenly had a home.

How to Spot (and Fix) Overly Coupled Code in Your Own Projects

You don't need a code review to spot trouble. Look for these red flags in your own projects:

  • The