After this lesson, you will be able to: Explain the difference between programming and software engineering, name the core engineering principles, and apply them to a small code sample.
Programming is making the machine do something. Software engineering is making the machine do something that you, your team, and your future self can keep working on for years. This lesson opens the sub-track by naming the principles that separate the two.
This is a free introductory lesson. No purchase required.
Programming is the act of writing code that runs. Software engineering is everything around it: choosing what to build, structuring it so other engineers can read it, testing it so you know it works, deploying it so users can use it, monitoring it so you notice when it breaks, and evolving it as requirements change. The same person writes both; the engineering mindset is what separates senior from junior.
If the same logic appears in three places, an update has to happen in three places. DRY says: identify the duplication, extract it into a function, module, or component, and call it from each place. The trap: premature DRY. Two pieces of code that LOOK similar but are conceptually different will diverge later, and pulling them into one shared helper handcuffs both. Wait for the third occurrence before extracting.
Single Responsibility: one class / module / function does one thing. Open/Closed: open to extension (add new behavior), closed to modification (don't break existing callers). Liskov Substitution: a subclass should work anywhere its parent works. Interface Segregation: don't force callers to depend on methods they don't use. Dependency Inversion: depend on interfaces, not concrete implementations. SOLID is most relevant in OO-heavy languages (Java, C#). In modern TypeScript or Python, the same ideas show up as small modules and pure functions.
Different parts of the system should do different things. Your HTTP route handler decodes the request and writes the response. Your business logic decides what to do. Your data layer reads and writes to the database. When concerns mix (SQL in a React component, validation in a database trigger), changes get expensive because everything is tangled.
Code should do what a reader expects from its name and shape. A function called `getUser(id)` should return a user (or null), not start a background thread that emails them. A method called `save()` should persist; not also send a notification, audit log, and Stripe charge. Surprise is bug-attractive. Lean toward boring, predictable behavior.
Identify the principles violated and propose a fix.
// BADfunction processUser(rawJson: string) {const data = JSON.parse(rawJson);if (data.age < 18) throw new Error("too young");const user = { id: crypto.randomUUID(), name: data.name, age: data.age };db.query(`INSERT INTO users VALUES ('${user.id}', '${user.name}', ${user.age})`);sendEmail(user.name, "Welcome!");console.log("saved " + user.id);return user;}// Issues to spot:// 1. Mixes parsing, validation, persistence, side effects (notification), logging.// 2. SQL injection vector via string interpolation.// 3. No error handling on db.query or sendEmail.// 4. Function name 'processUser' is vague (least surprise violation).// 5. Validation rule (age >= 18) is buried; should be at the boundary.// Better shape:// parseAndValidate(rawJson) -> User// saveUser(user) -> Promise<void>// notifyWelcome(user) -> Promise<void>// each function does one thing, errors handled at the boundary
Treating principles as rules to memorize rather than tools to choose between. Apply them when they reduce real pain; ignore them when the cost outweighs the benefit. Over-engineering tiny scripts to be 'production-grade'. A one-off migration script doesn't need SOLID. DRY-ing two things that LOOK similar but aren't conceptually the same. Coupling them will hurt later. Calling a function `process` or `handle` and burying every concern inside it. Names that don't promise specific behavior become surprise factories.
Pick the most surprising behavior.