Software Engineering and Project Management Tutorial from Scratch (2026)
Software engineering transforms programming from a solo craft into an industrial discipline. This tutorial covers the entire software development lifecycle: requirements gathering, architecture design, iterative development, testing, CI/CD, and maintenance. Drawing on my experience shipping production software at scale, I cover practical methodologies — Agile, Scrum, Kanban, DevOps — along with software design principles (SOLID, clean architecture) and project estimation techniques.
We will apply these concepts by building a project plan, designing a microservice architecture, and setting up a CI/CD pipeline.
Software Development Life Cycle (SDLC)
The SDLC encompasses: Requirements (gathering and analysis), Design (architecture and detailed design), Implementation (coding), Testing (unit, integration, system), Deployment, and Maintenance. The Waterfall model proceeds sequentially through phases. Agile methods use iterative cycles (sprints) with continuous feedback. Each model suits different project types.
from enum import Enum
class SDLCPhase(Enum): REQ=1; DESIGN=2; IMPL=3; TEST=4; DEPLOY=5; MAINT=6
class WaterfallProject:
def __init__(self): self.phases=[]; self.approved=True
def add_phase(self, p):
self.phases.append(p)
return self.approved
class Sprint:
def __init__(self, n, days): self.n=n; self.days=days; self.stories=[]; self.done=[]
def plan(self, backlog, capacity):
pts=0
for s in backlog:
if pts+s['pts']<=capacity: self.stories.append(s); pts+=s['pts']
for s in self.stories: backlog.remove(s)
def complete(self, story): self.done.append(story); self.stories.remove(story)
class AgileProject:
def __init__(self): self.backlog=[]; self.sprints=[]; self.velocity=0
def run_sprint(self, days):
cap=self.velocity*0.9 if self.velocity else 20; s=Sprint(len(self.sprints)+1,days)
s.plan(self.backlog,cap); self.sprints.append(s); return s
Requirements Engineering and User Stories
Requirements define what the system must do. Functional requirements describe features; non-functional requirements describe quality attributes (performance, security, scalability). User stories follow the format: 'As a [role], I want [goal] so that [benefit].' Acceptance criteria specify conditions for story completion. INVEST: Independent, Negotiable, Valuable, Estimable, Small, Testable.
from dataclasses import dataclass
@dataclass
class UserStory:
id: str; role: str; goal: str; benefit: str; pts: int; ac: list
def text(self): return f'As a {self.role}, I want {self.goal} so that {self.benefit}'
def is_done(self, results):
return all(ac in ['passed','completed'] for ac, res in zip(self.ac, results))
class Requirement:
def __init__(self, fid, desc, prio='M', nfr=None):
self.fid=fid; self.desc=desc; self.prio=prio; self.nfr=nfr or {}
def mo_scow(self): return {'M':'Must','S':'Should','C':'Could','W':'Wont'}.get(self.prio,'?')
# example
story=UserStory(story_id='US001', role='customer', goal='search products by name', benefit='find items quickly', pts=5, ac=['search field visible','results within 2s','partial match works'])
print(story.text())
Software Architecture: SOLID and Clean Architecture
Software architecture is the high-level structure of a system. SOLID principles: Single Responsibility (a class has one reason to change), Open/Closed (extend without modifying), Liskov Substitution (subtypes replace base types), Interface Segregation (small, focused interfaces), Dependency Inversion (depend on abstractions, not concretions). Clean Architecture layers: entities, use cases, interface adapters, frameworks.
from abc import ABC, abstractmethod
# Single Responsibility
class Order: pass
class OrderCalculator: # single reason: calculate totals
def calc(self, o): return sum(i.price for i in o.items)
class OrderRepository(ABC): # depends on abstraction
@abstractmethod; def save(self, order): pass
class OrderService: # depends on abstraction
def __init__(self, repo: OrderRepository): self.repo=repo
def place(self, order):
total=OrderCalculator().calc(order)
if total>0: self.repo.save(order)
class MySQLRepo(OrderRepository):
def save(self, order): print(f'saved order to MySQL')
class MongoRepo(OrderRepository):
def save(self, order): print(f'saved order to MongoDB')
# Open/Closed: add new tax calculator without changing OrderCalculator
class TaxCalculator(ABC):
@abstractmethod; def tax(self, o): pass
class VATTax(TaxCalculator):
def tax(self, o): return 0.2*sum(i.price for i in o.items)
Testing: Unit, Integration, and E2E
Testing ensures software quality. Unit tests verify individual functions or classes in isolation. Integration tests verify that components work together. End-to-end tests verify complete user workflows. The testing pyramid: many unit tests, fewer integration tests, few E2E tests. TDD (Test-Driven Development) writes tests before code: Red (fail), Green (pass), Refactor.
import unittest
class Calculator:
def add(self,a,b): return a+b
def div(self,a,b):
if b==0: raise ValueError('div by zero')
return a/b
def preq_condition(cond):
def deco(f):
def wrapper(*a,**kw):
if not cond: raise RuntimeError('precondition failed')
return f(*a,**kw)
return wrapper
return deco
class TestCalc(unittest.TestCase):
def setUp(self): self.c=Calculator()
def test_add(self): self.assertEqual(self.c.add(2,3), 5)
def test_add_neg(self): self.assertEqual(self.c.add(-1,1), 0)
def test_div(self): self.assertEqual(self.c.div(10,2), 5)
def test_div_zero(self): self.assertRaises(ValueError, self.c.div, 1, 0)
# mock example
from unittest.mock import Mock
repo=Mock(); repo.save.return_value=True
svc=OrderService(repo); print(svc.place(Order()))
CI/CD and DevOps Pipeline
CI/CD automates building, testing, and deploying software. Continuous Integration: merge code frequently, run automated tests on each push. Continuous Delivery: automatically deploy to staging, manual approval for production. Continuous Deployment: fully automated to production. The pipeline stages: source (git), build (compile), test (unit+integration), package (Docker image), deploy (Kubernetes).
import yaml
pipeline = {
'name':'build-test-deploy','on':{'push':{'branches':['main']}},
'jobs':{
'build':{
'runs-on':'ubuntu-latest',
'steps':[
{'uses':'actions/checkout@v4'},
{'name':'Install','run':'npm ci'},
{'name':'Lint','run':'npm run lint'},
{'name':'Test','run':'npm test -- --coverage'},
{'name':'Build','run':'npm run build'},
{'name':'Docker','run':'docker build -t app .'},
]
},
'deploy':{
'needs':['build'],'runs-on':'ubuntu-latest','if':'github.ref=="refs/heads/main"',
'steps':[
{'name':'Deploy','run':'kubectl set image deployment/app app=ghcr.io/org/app:latest'}
]
}
}
}
print(yaml.dump(pipeline, default_flow_style=False))
Project Estimation: COCOMO and Planning Poker
Software project estimation predicts effort, schedule, and cost. COCOMO II estimates person-months based on source lines of code (SLOC) and cost drivers (product, platform, personnel, project factors). Planning Poker is a consensus-based estimation technique where team members assign story points. The Delphi technique uses expert judgment in rounds.
import math
class COCOMOII:
def __init__(self, kloc, mode='organic'):
self.kloc=kloc; modes={'organic':(3.2,1.05),(semi':(3.0,1.12),'embedded':(2.8,1.20)}
self.a,self.b=modes.get(mode,(3.2,1.05)); self.eaf=1.0
def effort(self): return self.a * (self.kloc**self.b) * self.eaf
def schedule(self): return 2.5 * (self.effort()**0.38)
def staff(self): return self.effort() / self.schedule()
# Planning Poker
class PlanningPoker:
def __init__(self): self.cards=[1,2,3,5,8,13,21,40,100,'?']
def round(self, votes):
numeric=[v for v in votes if isinstance(v,int)]
if not numeric: return '?'
avg=sum(numeric)/len(numeric); return min(self.cards[:-1],key=lambda x:abs(x-avg))
pp=PlanningPoker(); consensus=pp.round([5,8,5,13,5])
print(f'Planning Poker results: avg=7.2, consensus={consensus}')
Frequently Asked Questions
What is the difference between Agile and Waterfall?
Waterfall is sequential with distinct phases; changes are costly. Agile is iterative with continuous feedback; changes are expected and accommodated. Agile suits uncertain requirements; Waterfall suits well-understood projects.
What is technical debt?
Technical debt is the implied cost of rework caused by choosing an easy solution now instead of a better approach that would take longer. Like financial debt, it accrues interest (harder maintenance).
What is the difference between unit and integration tests?
Unit tests verify individual components in isolation (often with mocks). Integration tests verify that multiple components work together (real databases, services). Both are essential for quality.
What is DevOps?
DevOps is the practice of combining software development (Dev) and IT operations (Ops), emphasizing automation, CI/CD, monitoring, and collaboration to shorten the development lifecycle.
Originally published on Ayodhyyya. Last updated June 1, 2026.