Software engineering, explained
Software Engineering
Software engineering is the discipline of turning a human need into a useful, dependable system, then keeping that system useful as the world changes. Programming is part of it, but programming alone is not the whole job. You can write elegant code that solves the wrong problem, or correct code that nobody can safely change. Engineering asks a wider question: what must we learn, decide, build, check and operate so that this software keeps doing the right thing?
A useful way to learn the discipline is to follow nine phases: Requirements, Design, Implementation, Testing, Code review, CI/CD, Deployment, Operations and Maintenance. They look like a line when you first meet them. In practice they form a loop. A production incident can reveal a missing requirement. A code review can expose a design assumption. A test can teach you that a word in the requirement meant two different things. Each phase sends information back to earlier phases.
A small software change travels through a learning loop
Each phase creates evidence that can improve an earlier decision, so delivery is a loop rather than a one-way conveyor belt.
- Need What should become better?
- Decision What behaviour and structure fit?
- Change What code makes it real?
- Evidence What did checks and users show?
- Learning What should change next?
The loop closes when evidence updates the next requirement.
Start with the need
Requirements make the problem visible
Imagine asking someone to build you a door. “Make it good” is not enough information. You need to know where it goes, who uses it, how wide it must be and what it should do when locked. Software requirements play the same role. They describe the problem, the desired behaviour and the boundaries that make an answer acceptable. The precise term is requirements engineering: discovering, recording and checking what a system needs to accomplish.
A requirement should be observable. “Make checkout fast” invites an argument about taste. “A returning customer can complete checkout in three steps, and the confirmation appears within two seconds on the supported connection” gives a team something to investigate and test. It may still need refinement, but its meaning can be challenged. Ask whose problem this solves, what happens in the ordinary case, what happens when data is missing, and how you will know the change helped.
TRY IT: Write requirements for your coffee routine. State who is making it, what “ready” means, what inputs can be missing, how long the process may take and what should happen if the first attempt fails. Then ask a friend to interpret your words without extra explanation. Their questions show where your requirement is still ambiguous.
Choose a shape
Design decides where the rules live
If requirements describe the destination, design chooses the map. Before building a house, you decide where rooms connect, where water enters and which walls carry weight. A software design does something similar. It divides a system into parts, gives each part a responsibility and defines how those parts communicate. The precise term is software architecture when the decisions concern the system's major boundaries, and design for the smaller structures inside them.
Suppose you are adding reminders to a note-taking app. One design stores a reminder inside each note and checks due reminders whenever a user opens the app. Another gives reminders their own records and runs a scheduled process. Both can satisfy “show me my due reminders,” but they differ when the user has the app closed, changes time zones or deletes a note. Design makes those consequences explicit before they are expensive to discover.
TRY IT: Draw the design for a to-do list on paper. Give one box responsibility for tasks, one for reminders and one for the screen. Draw the messages between them. For every arrow, write what data travels and what happens when the receiver is unavailable. Explain your drawing back to yourself in plain words. Any box you cannot explain probably has two jobs.
Turn decisions into behaviour
Implementation is where ideas meet reality
Implementation is the phase most people picture when they hear programming: editing files, writing functions, changing a database and connecting components. Think of it as building the door from the plan. The precise term is still implementation, but its engineering meaning is broader than typing code. It includes choosing names, handling invalid input, preserving data, using libraries and making the intended behaviour reproducible.
Start with the smallest vertical slice that proves the design. For the reminder app, that might mean saving one reminder, finding reminders due today and showing one clear state on screen. Do not begin by building every setting and notification channel. A small slice lets you discover whether the data shape, time calculation and user expectation agree. The first implementation is a question made executable.
There is no rule that implementation must follow the original design unchanged. The code may reveal that a boundary is awkward or a requirement is impossible under the chosen constraints. Bring that evidence back to the design and requirements phases. Engineering becomes expensive when people treat the first plan as sacred after reality has disproved it.
TRY IT: Write a function that turns a list of tasks into a morning checklist. First implement the ordinary case. Then deliberately break it with an empty list, a missing task name and a task due yesterday. Write down what behaviour you expected before fixing each failure. You are practising the boundary between an idea and an executable rule.
Ask the system questions
Testing turns confidence into evidence
A test is a question with a repeatable answer. You do not test whether a toaster is “nice.” You check whether it heats bread when switched on, stays off when switched off and handles the expected voltage. Software tests do the same for behaviour. Testing is the deliberate search for evidence that the system meets its requirements and fails safely outside them.
Use several distances. A unit test checks a small piece in isolation, such as whether a due-date function handles midnight correctly. An integration test checks parts working together, such as saving a reminder and reading it back. An end-to-end test follows a user journey through the real boundaries. These names describe scope, not quality. A large test can still check the wrong thing, and a small test can miss a broken connection.
Begin with the observable contract. For a reminder, you might require that a saved reminder appears once when due, does not appear before its due time and remains present if delivery fails. Include an edge case that would expose a likely mistake, such as daylight-saving time or a repeated request. A passing test is useful only when its expected result represents something the user or system actually needs.
TRY IT: Take your checklist function and write three questions before changing it: ordinary tasks, no tasks and a malformed task. Predict each answer, run the function, then compare. Add one test for the failure you most want to prevent. Ask a classmate to invent a fourth case. Their case tells you what your mental model left out.
Invite another mind
Code review tests the reasoning
Imagine writing an answer on a whiteboard, then asking another student to check not only the arithmetic but also whether you answered the right question. That is code review. A reviewer examines a proposed change for correctness, clarity, risk and fit with the surrounding system. The precise term is peer review, and its value comes from bringing a different context to the work.
Review the change, not the author's character. Start with the requirement and acceptance conditions. What behaviour changed? What data can be lost? What happens on retry? Which permissions are involved? Then inspect the implementation and the tests as evidence for those answers. A small diff is easier to reason about, but small does not mean safe. One changed line can alter billing, access or deletion behaviour.
The writer should not be the only judge. A self-review can catch spelling mistakes and missing files, but it often repeats the assumptions that shaped the patch. This is why the writer cannot be the judge is a useful principle for consequential work: separate creation from verification where the cost of a wrong change is high. Review is not a guarantee, but it creates a chance for disagreement before the change reaches users.
TRY IT: Give your checklist function to someone without explaining your implementation. Give them only the requirements and ask for three questions about the change. Do not defend the code while they inspect it. First classify each comment as correctness, risk, clarity or preference, then decide what evidence would settle it.
Make checks repeatable
CI/CD turns team agreements into a path
Suppose every student in a lab must wash equipment before leaving. A rule written on the wall helps, but a shared checklist and a final inspection make the rule more reliable. Continuous integration, or CI, is the practice of frequently combining changes and running automated checks. Continuous delivery, often paired with CI as CD, keeps a tested change ready to release. Some teams use continuous deployment to mean that passing changes release automatically.
A CI pipeline might install dependencies, check formatting, run tests, build the application and scan for known problems. The point is not to make the pipeline impressive. The point is to give every change the same basic questions. If the pipeline is slow, flaky or unclear, people learn to ignore its answer. A failed check should tell you what was tested, why it matters and how to reproduce the failure locally.
TRY IT: Make a tiny pipeline on paper for your checklist. Put checks in order: syntax, expected examples, edge cases and a manual question about whether the checklist is actually useful. Mark which checks could run automatically and which need a person. If a check fails, write the exact next action. Vague red lights train people to click past them.
Move the change carefully
Deployment crosses the boundary
Deployment is moving a software version into an environment where other systems and people depend on it. Carrying a finished machine from a workshop into a busy factory is a useful analogy. The machine may work in isolation, but the move must account for power, space, timing and a way to stop it. The precise term is deployment, and it includes configuration, data changes, permissions, release sequencing and rollback.
Separate deployment from release when you can. You might install code without turning on its new behaviour, then enable it for a small group or behind a feature flag. A canary release sends a limited amount of traffic to the new version so you can compare evidence before widening its reach. A rollback returns to a previous version, but rollback may not undo a database migration or an email already sent. Reversibility must be designed, not wished for after the alarm starts.
TRY IT: Pretend your checklist is a released web feature. Write a five-line release plan: who enables it, what metric should improve, what failure stops the rollout, how you disable it and what data cannot be restored. If your rollback depends on a step you have never tried, label it as an assumption and test that assumption in a safe environment.
Watch the living system
Operations keeps software honest in motion
Software does not finish when it reaches a server. A garden needs watering because real weather differs from the plan. A running system needs attention because traffic, dependencies, data and user behaviour change. Operations is the work of running software reliably: observing it, responding to incidents, managing capacity and protecting its boundaries. The precise shorthand is DevOps when development and operations share practices and responsibility across that path.
Observability gives you a useful view of what the system is doing. Logs record events, metrics measure quantities such as latency or error rate, and traces follow a request across services. An alert should point to a condition that needs action, not merely report that a graph moved. For reminders, a useful signal might be the percentage delivered within the promised time, alongside failed deliveries and queue depth.
TRY IT: Choose a real service you use. Find one visible sign that tells you it is healthy and one sign that tells you it is failing. Write the first three actions you would take after seeing the failure. Then ask: which fact would you need before deciding whether to roll back, wait or repair forward? You are practising operational thinking before owning a pager.
Keep the promise
Maintenance protects the system's future
Maintenance is not sweeping up after the “real” work. It is the ongoing work that keeps software compatible, understandable, secure and aligned with current needs. A bicycle needs oil, tyre checks and occasional replacement parts even when it still moves. Software needs dependency updates, bug fixes, data cleanup, performance work, documentation and removal of features that no longer earn their cost. The precise term is the software maintenance lifecycle.
Maintenance also closes the loop. A support question may become a clearer requirement. A slow query may require a design change. A repeated incident may produce a new test and deployment guard. The system teaches you what the original plan could not know. Your job is to make that learning durable in code, tests, records and habits, while removing old assumptions instead of stacking new ones on top.
TRY IT: Return to your coffee or checklist example. List one change in the outside world that could break it, one user request that could improve it and one part you would delete if nobody used it. Choose one small maintenance task and do it. Then update the requirement that explains why the task matters.
The loop now closes back at requirements. Testing, review, deployment and operations produce evidence. Maintenance preserves what you learned. That evidence can change the problem statement, which changes the next design and the next implementation. The phases are separate because each asks a different question, but they are connected because one answer changes what the next question should be.
Do not wait for a large product to practise this. Run one full tiny loop this week. Choose a small improvement, write an observable requirement, sketch its design, implement it, test it, ask someone to review it, run the checks, deploy it somewhere safe, observe what happens and record the maintenance task it creates. Then explain the loop back in your own words. That is software engineering: not merely making code run, but learning what should run and building a disciplined way to keep it right.