Software Engineering

20 min. read

Software Engineering

Most Value Skills
Divide and Simplify
Develop Good Mental Models
Learn Your Tools

The 3 Skills That Helped Me Become a Better Software Engineer

Victor Savkin is a co-founder of, providing Angular consulting to enterprise teams. He was previously on the Angular core team at Google, and built the dependency injection, change detection, forms, and router modules.

When asked “How to become a good software engineer?”, I recommend the following three skills:

Divide and Simplify
Develop Good Mental Models
Learn Your Tools
Don’t get me wrong, there are no shortcuts here — it takes many years of deliberate practice to become a decent engineer. The three skills above are something most good engineers practice. Almost without exception, when I interview a person who is good at the three skills, I know she is going to be a remarkable engineer.

Divide and Simplify
Since we are unable to keep more than 5–7 objects in working memory, any non-trivial problem is too large to think about in detail. We can either think about the problem abstractly, or think about a small part of the problem in detail. Both are important. It’s also important to be aware of the level of abstraction you are currently operating at. Either think abstractly, using metaphors and bulk approximations, or think precisely. Try not to mix the two.

This is the process I follow when dividing a problem into subproblems:

Step 1. Make sure you understand the problem.

State the problem on paper.
Write down all the constraints you know of.
Write down the things you don’t know that could have been helpful. Find them out.
Step 2. Draw a diagram of the problem domain.

A few boxes and arrows, nothing fancy.
This diagram should give you an idea on how to divide the problem into subproblems.
Find a way to draw a diagram that divides the problem domain differently. If you cannot do it, you probably don’t understand the problem domain well enough.
Pick one way of dividing the problem.
Step 3. Pick a subproblem and use the same process to subdivide it further.

Stop when problems (problem subdomain) are small and clear.
Solve them individually and combine the results.

Subdividing works well when dealing with deep problems because every division removes one layer. Real-world systems aren’t just deep, they are also broad.

This means that even though you might be at the right level of abstraction, there is still too much detail for you to solve the problem. In this case, take the problem and come up with a simple reproduction illustrating it. Solve it in this simplified context, and then apply the fix to the real-world problem.

Imagine you are investigating a performance issue. Say by dividing the problem over and over again, you figured out that the issue is with a grid component. Unfortunately, you have dozens and dozens of rows of components rendered by the grid that obscure your investigation. After spending an hour trying to solve it as is, step back to figure out a way to simplify it. Create a new application using this grid reproducing the issue. Make it as simple as possible (e.g., maybe one column showing text). Use it to fix the problem and then apply the fix to the real world situation.

Use Scientific Method
Use the scientific method to improve your understanding of the problem. It works as follows:

Write down the question you are trying to answer.
Write down a hypothesis.
Write down a prediction that is the result of the hypothesis.
Test the prediction and write down the result.
Repeat until the question is answered.
Imagine we created a simplified application using the grid component. Now we can use the scientific method to figure out why it is slow.

Write down the question: Why does it take 5 seconds to render the grid?.

After thinking about it for a while, we can get this idea: Maybe the change detection runs too many times?. This is our first hypothesis, so let’s write it down.

A prediction we can use to check the hypothesis is that appRef.tick will appear multiple in the dev tools profiler.

We run the experiment and the result confirms our hypothesis. Let’s write it down.

Now, to the next question: Why does the change detection run hundreds of times?.

Steps 1, 3, an 4 of this process are well defined. They can almost feel mechanical. Step 2 — coming up with a hypothesis — is where the creative work happens. And that’s where good mental models and intuitions are of great help.

Develop Good Mental Models
“Point of view is worth 80 IQ points” Alan Kay
A high school student who knows basic algebra, geometry and calculus can solve more math problems than talented mathematicians of ancient Rome. That’s not because the student is “smarter”. Her IQ isn’t higher. She can do it because she has good mental models.

Developing good mental models is one of the most important investments we can make as engineers. For instance, look at this list:

a programming language
a program
a compiler
type systems (optional, mandatory)
functional programming, imperative programming, logical programming
a vm
an interpreter
a garbage collector
a database
distributed systems

Do you have good mental models of all of them?

Having a good mental model about X, does not mean you have to be an expert in X. It means that you can draw a diagram showing how X works, and, if given enough time, can build a simple version of X.

The mental models above are generic. These are some frontend-oriented mental models:

Change detection
Observable objects
Virtual DOM
CQRS/event sourcing/Redux

Develop Mental Models
How do you develop good mental models?


First, you read a lot. Read books, read papers.

Use “Narrow” Technologies

Second, find a piece of technology that does just that thing you are trying to learn and does only that. If you are trying to learn functional programming, pick up Elm, and spend a week trying to solve problems with it. Since Elm is pure, it won’t let you take shortcuts — you will have to do it by the book. You also won’t be distracted by the complexity of a more generic tool, such as Scala.

Build It Yourself

Finally, build a simplified version of what you are trying to learn from scratch. Learning about a compiler? Build your own compiler. Learning about logical programming? Build your own prolog interpreter. Learning about distributed systems? Build a tiny program illustrating message passing between different nodes in the system. Use it to learn about CAP.

It takes a year to build a compiler for a mainstream programming language. That’s because you want it to be fast, always correct, and handle all the weird corner cases of that language. Take a simpler language (e.g., some variation for lisp), ignore performance issues, you can do it in a day.

Note, this is play, so you cannot fail at this. You don’t have to ship anything. The only purpose of this is your learning. So even if you haven’t finished your compiler, but you learned a lot — you succeeded.

Learn Your Tools
Software engineers are professionals that rely on tools. These are some of the tools I use every day:

Editors and IDEs
Package managers (brew, npm, yarn, …)
Languages (typescript, css, …)
Dev tools (Chrome Dev tools, node debugger, source maps, …)
Frameworks and tools (angular, rxjs, webpack, rollup, …)

Whereas mental models are abstract, tools are concrete. You can be good at VSCode, but know nothing about VIM, even though both are text editors. It takes months or even years to learn a new keyboard layout or master an IDE. That’s why this list is a lot narrower.

There are two reasons to learn your tools well:

You are more effective at executing tasks if you know the right tools. If you know Angular well, you can build a simple app in an hour, without having to Google stuff. Similarly, you will be able to troubleshoot an issue with a debugger much faster comparing to debugging with console.log.

An even more important reason to learn your tools well is so you can use them without thinking. Take editor shortcuts. Using editor shortcuts is better not because it is faster (even though it is), but because you can use them automatically, without having to go through menus, without thinking about it at all. So your conscious mind is free to think about the real problem.

Being good at your tools doesn’t mean everything you use has to be custom. I use an ergonomic programmable keyboard because I spent a lot of time thinking about how my fingers move while typing. You don’t have to go that far. Just get good at whatever you use.

I find the following to be true:

Every engineer who knows their tools well is a good engineer. A person that doesn’t know their tools well is very unlikely to be a good engineer.

You Need to Be More Than a Good Engineer to Make an Impact
Mastering the three skills doesn’t mean you will be effective at your job or will make a substantial impact. Being able to troubleshoot a bug or write a new library is not enough to make an impact. These are some other skills that matter just as much, if not more:

Your social skills. Most interesting projects are built by teams. So they are built by people with different skills, backgrounds, and personalities. You need to have strong social skills to work in such environments.
Your writing skills. You have to communicate your ideas to your team, your clients, and the community. Writing is the most scalable way to do it. Practice writing.
Your work ethic.

These are the foundational skills of a good software engineer:

The “Divide and Simplify” skill helps us tackle complexity. This is how you think, and it is the most fundamental skill upon which everything is built. We use it when learning technologies, writing software, and debugging issues.
The “Learn Good Mental Models” skill helps us formulate hypotheses while debugging and helps us create novel solutions. This is the foundation that enables creativity.
The “Learn Your Tools” skill help us execute ideas: run experiments, write software. It also helps us unload all the trivial aspects of our work, so we can focus our conscious mind on non-trivial problems.

General Software Engineering
Jacques Leemans

Agile software development
Agile software development describes a set of values and principles for software development under which requirements and solutions evolve through the collaborative effort of self-organizing cross-functional teams. It advocates adaptive planning, evolutionary development, early delivery, and continuous improvement, and it encourages rapid and flexible response to change.[2] These principles support the definition and continuing evolution of many software development methods

Design Patterns
Creational patterns are ones that create objects for you, rather than having you instantiate objects directly. This gives your program more flexibility in deciding which objects need to be created for a given case.
Abstract factory pattern
groups object factories that have a common theme.
Builder pattern
constructs complex objects by separating construction and representation.
Factory method pattern
creates objects without specifying the exact class to create.
Prototype pattern
creates objects by cloning an existing object.
Singleton pattern
restricts object creation for a class to only one instance.
These concern class and object composition. They use inheritance to compose interfaces and define ways to compose objects to obtain new functionality.
allows classes with incompatible interfaces to work together by wrapping its own interface around that of an already existing class.
decouples an abstraction from its implementation so that the two can vary independently.
composes zero-or-more similar objects so that they can be manipulated as one object.
dynamically adds/overrides behaviour in an existing method of an object.
provides a simplified interface to a large body of code.
reduces the cost of creating and manipulating a large number of similar objects.
provides a placeholder for another object to control access, reduce cost, and reduce complexity.

Most of these design patterns are specifically concerned with communication between objects.
Chain of responsibility
delegates commands to a chain of processing objects.
creates objects which encapsulate actions and parameters.
implements a specialized language.
accesses the elements of an object sequentially without exposing its underlying representation.
allows loose coupling between classes by being the only class that has detailed knowledge of their methods.
provides the ability to restore an object to its previous state (undo).
is a publish/subscribe pattern which allows a number of observer objects to see an event.
allows an object to alter its behavior when its internal state changes.
allows one of a family of algorithms to be selected on-the-fly at runtime.
Template method
defines the skeleton of an algorithm as an abstract class, allowing its subclasses to provide concrete behavior.
separates an algorithm from an object structure by moving the hierarchy of methods into one object.

Dont Repeat Yourself!
This counts for function, classes, functionality, for loops, and general repetitive code.

SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable
Single responsibility principle (SRP)
a class should have only a single responsibility (i.e. changes to only one part of the software’s specification should be able to affect the specification of the class).
Open-closed principle (OCP)
“software entities … should be open for extension, but closed for modification.” Whenever you create a bunch of switch statements or if statements, it’s probably time to change. Abstract classes and interfaces are good examples of this.
Liskov substitution principle (LSP)
“objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.” See also design by contract.
Interface segregation principle (ISP)
many client-specific interfaces are better than one general-purpose interface. Add different interfaces for different functionality.
Dependency inversion principle (DIP)
one should “depend upon abstractions, [not] concretions.”
Dependency injection.
3 Types:
Constructor Injection
Property Injection
Method Injection



View only, like a text field, list, etc..
View with interactivity, like a button, Input Field,
Controller without a view.

Rapid Application Development (RAD)

Automated Unit Testing
Where possible, setup automated testing

Source Code Management SCM

Continuous Integration
integrate code early and often


DevOps is a set of practices intended to reduce the time between committing a change to a system and the change being placed into normal production, while ensuring high quality
As DevOps is intended to be a cross-functional mode of working, rather than a single DevOps tool there are sets (or “toolchains”) of multiple tools.[13] Such DevOps tools are expected to fit into one or more of these categories, reflective of key aspects of the development and delivery process
Code — code development and review, source code management tools, code merging
Build — continuous integration tools, build status
Test — continuous testing tools that provide feedback on business risks
Package — artifact repository, application pre-deployment staging
Release — change management, release approvals, release automation
Configure — infrastructure configuration and management, Infrastructure as Code tools
Monitor — applications performance monitoring, end–user experience






Design Patterns

Agile Software Development

Unit Testing

Continuous Integration


Design by Contract

Version Control

Continuous Testing

Binary Repository Management

Software Development Process



Single responsibility principle

Open/closed principle

Liskov substitution principle

Interface segregation principle

Dependency inversion principle

You aren’t gonna need it

Adaptive software development

Code Reuse

Computer Programming

Object-oriented programming


Package Principles