Lesson 3 - The Software Development Lifecycle
On this page
- Welcome to The Software Development Lifecycle
- Requirements Analysis: Deciding What to Build
- Design: Deciding How to Build It
- Implementation: Writing the Code
- Testing: Verifying It Actually Works
- Deployment: Releasing It to Real Users
- Maintenance: Keeping It Working After Launch
- Sequential or Iterative? How the Phases Connect
- Practice Exercises
- Summary
- Next Steps
- Continue Building Your Skills
Welcome to The Software Development Lifecycle
The Ledgerly team has a new idea. Right now, someone on the team has to notice an overdue invoice and manually trigger a reminder email. They want the app to do this by itself: find every overdue invoice, every day, and email the customer without anyone asking it to. That sounds like a small feature. Building it well still means passing through the same stages every software feature passes through, from a vague idea to code running safely in front of real customers.
Those stages are called the software development lifecycle, or SDLC: a repeatable path a feature travels from “someone has an idea” to “customers are using it and the team keeps it healthy.” This lesson walks the three-person Ledgerly team through all six phases, using the automated reminder feature as the thread that ties every phase together.
By the end of this lesson, you will be able to:
- Name the six phases of the SDLC and describe what each one produces
- Turn a vague feature idea into concrete requirements, the way the Ledgerly team does
- Explain what separates a design decision from an implementation decision
- Explain why testing and deployment are distinct phases with different goals
- Describe why the SDLC behaves as a repeating cycle rather than a one-time checklist
Requirements Analysis: Deciding What to Build
Requirements analysis is the phase where a team turns a rough idea into a specific, written description of what the software must do. Skip it, and everyone builds a slightly different feature in their head.
For Ledgerly, “automatic reminders” is not yet a requirement — it is a wish. The team has to answer specific questions first. How overdue does an invoice need to be before a reminder goes out? Does a customer get one reminder or several? What happens if the customer pays five minutes before the reminder job runs? Answering these questions turns a wish into a requirement: a statement precise enough that two different developers would build the same thing from it.
After a short conversation with two beta customers, the Ledgerly team writes this down:
- An invoice becomes eligible for a reminder the day after its due date, if it is still unpaid.
- Each eligible invoice gets exactly one reminder email per day, until it is paid.
- The reminder email includes the invoice amount, the due date, and a payment link.
- The feature must not email a customer twice for the same invoice on the same day, even if the job runs more than once.
Notice what this list does not say: it says nothing about which programming class sends the email or how the check for “overdue” is implemented. Requirements describe what the system must do, from the customer’s point of view. Design, the next phase, decides how.
Functional and non-functional requirements
The four bullets above are functional requirements: specific behaviors the software must perform. Ledgerly also has non-functional requirements for this feature, like “the daily reminder check must finish in under two minutes” and “reminder emails must never be sent to a customer who unsubscribed.” Functional requirements describe what the system does; non-functional requirements describe how well it must do it.
Design: Deciding How to Build It
Design takes an agreed-upon requirement and turns it into a plan for the software’s structure, before anyone writes the feature’s code. A good design names the pieces involved and how they talk to each other, so implementation becomes a matter of following the plan rather than inventing it while coding.
For the reminder feature, the Ledgerly team sketches three pieces. An InvoiceRepository already knows how to fetch invoices from the database; it needs one new method that returns every unpaid invoice whose due date has passed. A small scheduled job runs once a day, asks the InvoiceRepository for that list, and for each overdue invoice, hands it to NotificationService, which formats the reminder email and sends it. NotificationService only sends email today, which is a deliberate, simple choice rather than a gap — the team is not building SMS or push support until a customer actually asks for it.
This is also where the team decides how to satisfy “never send two reminders for the same invoice on the same day.” They agree the job will record the date it last reminded each invoice, and skip any invoice already reminded today. That single design decision, made before any code exists, prevents an entire category of bug that would be much more expensive to fix after the feature ships.
Implementation: Writing the Code
Implementation is where the design becomes real, running code. This is the phase most people picture when they hear “software development,” but it is only one of six, and it goes fastest when requirements and design were done well first.
Following the design above, a Ledgerly developer writes the function that decides which invoices need a reminder today:
from dataclasses import dataclass
from datetime import date
@dataclass
class Customer:
name: str
email: str
@dataclass
class Invoice:
id: str
customer: Customer
amount_due: float
due_date: date
paid: bool
def invoices_needing_reminder(invoices, today):
"""Return unpaid invoices whose due date has already passed."""
return [inv for inv in invoices if not inv.paid and inv.due_date < today]
today = date(2026, 7, 5)
priya = Customer(name="Priya Shah", email="[email protected]")
invoices = [
Invoice("INV-1001", priya, 450.00, date(2026, 6, 20), paid=False),
Invoice("INV-1002", priya, 120.00, date(2026, 7, 10), paid=False),
Invoice("INV-1003", priya, 300.00, date(2026, 6, 1), paid=True),
]
for inv in invoices_needing_reminder(invoices, today):
print(f"Reminder needed: {inv.id} for {inv.customer.name}, "
f"${inv.amount_due:.2f} overdue since {inv.due_date}")Running this against three sample invoices produces exactly one reminder, which matches what the design intended:
Reminder needed: INV-1001 for Priya Shah, $450.00 overdue since 2026-06-20INV-1002 is skipped because its due date is still in the future, and INV-1003 is skipped because it is already paid. The function only decides which invoices qualify; wiring it into the real InvoiceRepository and NotificationService is more implementation work, built the same way, one small piece at a time.
Testing: Verifying It Actually Works
Testing is the phase where the team checks that the code from implementation actually satisfies the requirements from the first phase, rather than trusting that it does. A feature that looks right when a developer glances at it can still fail on cases nobody thought to check by eye.
For invoices_needing_reminder(), the Ledgerly team writes an automated test that checks three cases at once: an invoice that is genuinely overdue, one that is not due yet, and one that has already been paid.
from dataclasses import dataclass
from datetime import date
@dataclass
class Customer:
name: str
email: str
@dataclass
class Invoice:
id: str
customer: Customer
amount_due: float
due_date: date
paid: bool
def invoices_needing_reminder(invoices, today):
"""Return unpaid invoices whose due date has already passed."""
return [inv for inv in invoices if not inv.paid and inv.due_date < today]
def test_invoices_needing_reminder():
test_customer = Customer(name="Test Customer", email="[email protected]")
overdue = Invoice("A", test_customer, 100.0, date(2026, 1, 1), paid=False)
upcoming = Invoice("B", test_customer, 100.0, date(2026, 12, 1), paid=False)
settled = Invoice("C", test_customer, 100.0, date(2026, 1, 1), paid=True)
result = invoices_needing_reminder([overdue, upcoming, settled], date(2026, 7, 5))
assert result == [overdue], "Only the unpaid, past-due invoice should need a reminder"
print("All tests passed.")
test_invoices_needing_reminder()All tests passed.This one test is a unit test: it checks one small function in isolation, with no real database or email server involved. Ledgerly also runs integration tests that check the whole path — repository, reminder logic, and notification service working together — because a function that passes on its own can still fail once it is wired into the rest of the system.
Deployment: Releasing It to Real Users
Deployment is the phase where finished, tested code moves out of the development environment and starts running where real customers can reach it. Passing tests on a developer’s laptop is not the same as working correctly in production, so this phase gets its own deliberate steps.
The Ledgerly team does not flip the reminder feature on for every customer at once. They deploy it behind a feature flag, a setting that lets them turn the feature on for a small group first — their two beta customers — while everyone else sees no change. For a few days, the team watches whether reminders go out at the right time, whether the email content looks right, and whether the “no duplicate reminder” rule actually holds under real data. Only after that quiet trial period do they turn the flag on for every Ledgerly customer.
This staged approach exists because deployment is the first time the feature meets conditions nobody can fully simulate in testing: real customer data, real timing, and real email inboxes.
Maintenance: Keeping It Working After Launch
Maintenance is everything the team does to a feature after it has shipped, for as long as customers keep using it. Software is never really “done” the moment it deploys; it keeps needing attention as the world around it changes.
Maintenance work on Ledgerly’s reminder feature falls into a few recognizable buckets. Corrective maintenance fixes a real bug, like a reminder email that shows the wrong due date in one timezone. Adaptive maintenance responds to something outside the code changing, like the email provider changing its sending limits. Perfective maintenance improves something that already works, like adding the customer’s name to make the email feel less generic. Preventive maintenance heads off future trouble, like updating a dependency before it becomes unsupported. All four keep the feature reliable long after the celebratory launch message in the team chat.
Sequential or Iterative? How the Phases Connect
Two teams can follow the same six phases and still work very differently, depending on whether they move through those phases once per feature or many times.
A strictly sequential approach finishes requirements completely, then moves to design, then implementation, then testing, then deployment, never returning to an earlier phase for the current feature. This suits work where requirements are genuinely stable and changing course later is expensive, such as software controlling medical equipment. An iterative approach instead moves through all six phases quickly for a small slice of the feature, learns from the result, and repeats. Ledgerly works this way in practice: they built and shipped basic email reminders first, then iterated with a second small cycle to add the “no duplicate reminder” safeguard, rather than trying to design every edge case up front.
Either way, the six phases themselves do not change. What changes is how many times, and how quickly, a team moves through them before calling a feature finished. The maintenance phase is what keeps this from being a one-way trip: real usage always surfaces requirements nobody wrote down the first time, and those findings become the seed of the next cycle.
Practice Exercises
Exercise 1: Turn a wish into a requirement
A Ledgerly teammate says, “customers should get reminded about overdue invoices.” Rewrite this as two or three concrete functional requirements, in the style used earlier in this lesson.
Hint
Aim for statements specific enough that two developers would build the same thing, such as: “An invoice becomes eligible for a reminder the day after its due date, if unpaid,” “Each eligible invoice gets exactly one reminder email per day,” and “The reminder email includes the amount due, due date, and a payment link.” Notice that none of these mention a class name or a database query — that detail belongs to design, not requirements.
Exercise 2: Separate design from implementation
Ledgerly’s design says: “a scheduled job asks InvoiceRepository for overdue invoices once a day and hands each one to NotificationService.” Which of the following belong to design, and which belong to implementation: (a) deciding a job runs once a day rather than once an hour, (b) writing the actual Python function that filters the invoice list, (c) deciding NotificationService is the component responsible for sending email?
Hint
(a) and (c) are design decisions: they describe the shape and responsibilities of the system before any code exists. (b) is implementation: it is the actual code that carries out what the design described. A useful test is to ask, “does this decision survive being rewritten in a different programming language?” Design decisions usually do; implementation details usually do not.
Exercise 3: Diagnose a maintenance report
A support ticket says: “Ledgerly’s reminder emails now show amounts in the wrong currency after we added European customers last month.” Is this corrective, adaptive, perfective, or preventive maintenance, and why?
Hint
This is adaptive maintenance: the code did not have a bug when it was written, but something outside the code changed — Ledgerly started serving customers in a new currency — and the software now needs to adapt to that new reality. Corrective maintenance would apply if the currency logic had always been wrong, even for the original customers.
Summary
The software development lifecycle breaks a feature’s journey into six phases. Requirements analysis turns a vague idea, like “automatic reminders,” into precise statements about what the software must do. Design decides the structure that satisfies those requirements, such as which component checks for overdue invoices and which one sends the email. Implementation turns that design into real, running code. Testing checks that the code actually does what requirements demanded, rather than assuming it. Deployment releases the finished feature to real customers, often gradually through a feature flag. Maintenance keeps the feature healthy afterward through corrective, adaptive, perfective, and preventive work. Teams can move through these phases once per feature or repeatedly in fast iterations, but the six phases themselves stay the same, and maintenance findings routinely become the next cycle’s requirements.
Key Concepts
- Requirements analysis — turning a rough idea into precise, written statements of what the software must do.
- Design — deciding the structure and responsibilities of the solution before writing its code.
- Implementation — writing the real code that carries out the design.
- Testing — verifying, through unit and integration tests, that the code meets the requirements.
- Deployment — releasing finished, tested code to real users, often gradually.
- Maintenance — corrective, adaptive, perfective, and preventive work that keeps a shipped feature healthy.
- Sequential vs. iterative — moving through the six phases once per feature versus repeatedly in small cycles.
Why This Matters
Every engineer eventually works on a team, and every team’s work moves through some version of these six phases, whether they name them out loud or not. Knowing the phases gives you a shared vocabulary: “we haven’t finished requirements yet” explains a stalled feature far better than “things are unclear,” and “this is a maintenance fix” sets different expectations than “this is a new feature.” It also tells you where a problem actually lives — a wrong feature built well is a requirements failure, and a right feature built badly is an implementation or testing failure. The next lesson looks at development methodologies, the different ways teams choose to organize their movement through these same six phases.
Next Steps
Lesson 4: Development Methodologies
Compare Waterfall, Agile, and other ways teams organize their movement through the SDLC's six phases.
Back to Module Overview
Return to the Engineering Foundations module overview
Continue Building Your Skills
You now have the six phases of the software development lifecycle, seen through one real Ledgerly feature from a customer’s wish to a shipped, maintained reminder email. Next, you will look at development methodologies: the different ways teams like Ledgerly’s choose to move through these same six phases, from strict Waterfall sequencing to fast Agile iterations.