Computer science | Building software that lasts
Software engineering: building dependable software together
Go beyond writing code: discover how teams turn needs into maintainable systems through requirements, design, testing, collaboration, delivery, and feedback.
What software engineering means
Software engineering applies systematic practices to building, changing, and operating software. A short script can be written by one person in an afternoon; a service used by many people over years needs planning, testing, documentation, monitoring, and a way to change it safely.
Engineering balances several qualities: correctness, reliability, security, usability, performance, accessibility, and cost. No single quality can be maximized in isolation. For example, adding a feature quickly may increase maintenance cost, while delaying every release for a theoretical perfect design may prevent users from receiving useful improvements.
- People: users, developers, operators, and others affected by the software.
- Process: how a team plans and checks changes.
- Product: code, data, interfaces, documentation, and dependencies.
- Context: constraints, risks, laws, and operating environment.
Good engineering is not bureaucracy for its own sake. Its practices should reduce uncertainty and help teams deliver useful behavior reliably.
Requirements and acceptance criteria
A requirement describes a need or constraint. Functional requirements describe what a system should do; non-functional requirements describe qualities such as response time, availability, accessibility, and privacy. Requirements should be specific enough to discuss and verify.
“Make search fast” is ambiguous. “For a catalog of 10,000 items, return matching results within 300 milliseconds for typical queries on the agreed test environment” is more measurable. Real targets still need user research and realistic conditions, but they make acceptance testable.
Worked example: adding a saved list
A vague request might say, “Let users save favorite books.” Before implementation, ask: can a signed-out visitor save items? Are there limits? Can a user remove an item? What happens when a book is no longer available? Are saved lists private?
One acceptance criterion could be: “Given a signed-in reader viewing a book, when they choose Save, the book appears in their private saved list and remains there after they sign in again.” Additional criteria should cover duplicate saves, removing a book, unavailable books, and access by a different account.
Acceptance criteria turn a conversation into observable outcomes. They should describe user value, not prescribe unnecessary implementation details. Use examples and prototypes when written requirements alone leave ambiguity.
Scope and prioritization
Every feature has a cost and opportunity cost. A team can prioritize by expected user benefit, risk reduction, urgency, and effort. A smaller release that solves one important problem well can be more useful than a broad feature set that is difficult to test and maintain.
Design and architecture
Software architecture describes a system's major parts, their responsibilities, and how they communicate. Good design separates concerns so that one kind of change does not require rewriting unrelated areas. Modules should have clear interfaces and avoid unnecessary dependencies on internal details.
Choose boundaries based on change
Suppose a saved-list feature has a user interface, application rules, and persistent storage. The interface handles display and input; application logic enforces rules such as ownership and duplicate behavior; a data-access component reads and writes records. These boundaries make it easier to test the rules without rendering a page and to change storage without redesigning the interface.
Boundaries are useful only when they clarify responsibilities. Creating an interface for every trivial function adds indirection and can make a small program harder to follow. Start with a simple design, then introduce structure where there is a real reason.
Trade-offs and design records
Design choices have consequences. A monolithic application can be simpler to build and deploy; separate services can scale or evolve independently but introduce network failures and operational overhead. A brief architecture decision record can state the context, options, chosen approach, and trade-offs, giving future maintainers the reasoning behind an important decision.
Code quality and maintainability
Readable code helps people understand behavior and make safe changes. Use meaningful names, small cohesive functions, consistent formatting, and comments that explain surprising decisions rather than restating the code. Remove obsolete code and keep the structure aligned with the concepts the product actually has.
Maintainability is the ease with which a system can be understood, corrected, and extended. A useful design makes invalid states difficult to represent, keeps related rules together, and exposes clear errors. Documentation is most valuable when it explains setup, interfaces, constraints, and decisions that are not obvious from code.
Code review as shared quality work
Reviewers can ask whether a change meets its requirement, handles important edge cases, fits the design, protects data, and includes suitable tests. Reviews are also a learning opportunity. Specific, respectful comments about behavior are more actionable than comments about a developer's ability.
Review is not a substitute for automated tests, and tests are not a substitute for review. Each catches different problems: tests check selected behavior repeatedly; review can notice unclear assumptions, unsafe design, or maintenance costs beyond the test cases.
Testing at several levels
Testing provides evidence that software behaves as intended. A unit test checks a small function or component; an integration test checks interactions between components; an end-to-end test exercises a user-visible workflow through the assembled system.
| Test level | Example | Strength and limitation |
|---|---|---|
| Unit | Saving the same book twice follows the duplicate rule | Fast feedback; may use simplified dependencies |
| Integration | Saved-list rules work with the database | Checks real boundaries; needs more setup |
| End-to-end | A signed-in reader saves and later removes a book | Validates a complete flow; can be slower and more fragile |
| Exploratory | A person tries unusual sequences and unclear states | Can reveal surprises; results need recording and follow-up |
Tests should cover typical behavior, boundaries, failures, and important security rules. A passing test suite proves only that the checked cases passed; it does not prove that the requirements were complete or every possible input is correct.
Test doubles and independence
A test double replaces a real dependency with a controlled substitute. A fake database can make a unit test fast and repeatable, but it does not reveal every issue in the real database integration. Keep a smaller set of integration tests to verify actual boundaries, and ensure tests clean up the data they create.
Flaky tests sometimes pass and sometimes fail without a relevant code change. Common causes include timing assumptions, shared state, network dependencies, and tests that depend on execution order. Diagnose the source rather than repeatedly rerunning failures until they pass.
Version control and collaboration
Version control records the history of a project's changes. Git is a widely used distributed version-control system. A repository stores files and commits; branches let developers develop related changes separately before integrating them.
Small, focused commits are easier to review and revert. A useful commit message explains the intent of the change. Before sharing work, inspect the diff, run relevant tests, and verify that generated files or secrets were not added accidentally.
Merge conflicts are a collaboration signal
A conflict occurs when Git cannot automatically combine edits to the same part of a file. The tool does not know which meaning is correct. Read both changes, understand the feature intent, combine or choose the appropriate behavior, then run tests. Deleting conflict markers without understanding the change can silently remove required work.
Teams also benefit from clear ownership and communication. Describe how to run the project, record decisions, and raise blockers early. Shared tools help coordinate people; they do not replace agreement about interfaces and behavior.
Build, release, and operate
A build turns source files and dependencies into something runnable or distributable. A continuous-integration system can build and test each proposed change automatically. A release delivers a version to users; deployment puts that version into an environment where it can run.
Keeping development, test, and production environments similar reduces surprises. Configuration and secrets should be supplied through controlled environment-specific mechanisms rather than hard-coded into source. A gradual rollout can limit impact and make it easier to detect a regression before every user receives a change.
Observe the product in use
Logs, metrics, and traces help operators understand system behavior. Metrics summarize trends such as error rates and response time; logs record events; traces connect work across components. Collect only what is needed and control access, because operational data can contain private information.
Define meaningful service indicators before an outage. A service-level objective might set a target for successful requests or response time over a period. Such objectives help teams balance reliability work and feature development, but measurements should reflect user experience rather than a metric that is easy to improve without real benefit.
Maintenance and technical debt
Software changes because user needs, platforms, dependencies, and operating conditions change. Maintenance includes correcting defects, adapting to new environments, improving performance, and removing obsolete behavior.
Technical debt describes future costs created by a shortcut or design compromise. Like financial debt, some trade-offs are deliberate and worthwhile; hidden debt becomes dangerous when nobody knows why it exists or when it prevents safe changes. Record the impact and revisit high-cost compromises during planning.
Dependencies require ongoing maintenance. Teams need to know what packages and services their application relies on, which versions are in use, and how updates are tested. A reproducible build helps diagnose differences between a developer's machine and deployed software.
Responsible engineering
Software affects people differently. Consider accessibility, language, privacy, security, and the consequences of errors. A feature can technically meet its specification while excluding users or making harmful assumptions. Include representative users in design and testing when appropriate, and provide ways to report and correct problems.
Minimize personal data, explain important behavior, and give users meaningful control. If automation makes a consequential decision, consider whether people can understand, challenge, or appeal it. Accessibility and safety are core quality requirements, not decorations to add at the end.
A compact glossary
- Requirement
- A need, behavior, or constraint a software system should satisfy.
- Architecture
- The major components of a system and the relationships between them.
- Regression
- A previously working behavior that breaks after a change.
- Integration test
- A test that checks interactions across component boundaries.
- Deployment
- Making a software version available in a target environment.
- Technical debt
- Future change cost associated with a shortcut or design compromise.
References and further reading
- Ian Sommerville. Engineering Software Products: An Introduction to Modern Software Engineering. Pearson, 2020. Author resources.
- Martin Fowler. Refactoring: Improving the Design of Existing Code, 2nd edition. Addison-Wesley, 2018. Book information.
- Google. Software Engineering at Google. Free online book.
- ISTQB. Certified Tester Foundation Level syllabus. Testing concepts.
Try it yourself
- Turn “make a study planner” into three functional requirements and two non-functional requirements.
- Write acceptance criteria for marking one task complete, including an error case.
- Choose a feature and describe one unit test, one integration test, and one end-to-end test.
- Explain one deliberate technical-debt trade-off and how a team could track it.
- Suggest a service metric that reflects user experience and explain what it could miss.
Remember: strong software comes from repeated learning—understand needs, build a small change, verify it, and observe its effect.
Keep exploring
Try another topic
A fresh topic suggestion is ready for you.
Explore a topic