Golden software engineering quotes which best reveal most important axioms of modern software. Most of them can be found online. If you have a tough decision to make, try to go through this list to see if you can gather any clues about what decision might be the better one.
- Productivity
- If you pile up enough tomorrows then you’ll be left with nothing but a bunch of empty yesterdays.
- A few weeks of programming can save you hours of planning.
- Don’t confuse activity with an outcome.
- Start with the hardest problem first
- Don’t put off doing something that’s difficult or painful. Instead, we do it more often so that we figure out how to make it routine.
- Quality
- Management
- Incentives are counterproductive.
- Most meetings should be optional but inclusive.
- In a hierarchy every employee tends to rise to his level of incompetence.
- Above all, resist the urge to manage.
- If you cannot solve a problem without programming you cannot solve a problem with programming.
- Traditional managers worry about how to get things done, whereas great managers worry about what things get done and trust their team to figure out how to do it.
- Hope is not a strategy.
- A people hire other A people; B people hire C people.
- It’s easier to ask for forgiveness than it is to get permission.
- Testing
- Lack of failure is not a success
- Strive for Unchanging Tests.
- DAMP is superior to DRY.
- Decouple the places where people make the most mistakes from the places where they can cause failures.
- The acronym TDD should be taken to mean testability-driven design.
- If you don’t trust a library, don’t use it.
- Architecture
- Everything in software architecture is a trade-off.
- The architecture of a real-world running system is never clean or pristine.
- A good architect maximizes the number of decisions not made.
- Too strict design cripples the ability to evolve but with too much freedom, we don’t have a design at all.
- A structure is stable if cohesion is strong and coupling is low.
- Why is more important than how.
- The choice between availability and consistency is driven by the business requirements - least cost optimization.
- Scale comes from simplicity.
- Successful abstractions are the simple ones.
- Coupling comes in many forms.
- APIs should make it easy to do things right and make it hard to do them wrong.
- The data always outlives code.
- Design your data model for the sake of your domain model, not your domain model for the sake of your data model.
- We always abstract away things we cannot comprehend.
- Elegant solutions are properties of constrained problems.
- Having a language that allows you to do anything is not an advantage, it is a liability. You have to find the heart and essence and build on top of that.
- Software is built not like architecture.
- We are far to easily impressed and imprisoned by the concept of size.
- Focus on what is essential, not on what is incidental.
- Avoid cargo-cult programming - software presents diseconomy of scale
- All code is technical debt, some just have more interest rate.
- Dead code may be not so dead at some point and be zombie causing trouble.
- Architecture is like garden - you can grow things but you have to cut other things down.
- The major difference between a thing that might go wrong and a thing that cannot possibly go wrong is that when a thing that cannot possibly go wrong goes wrong it usually turns out to be impossible to get at or repair.
- Software becomes an artifact of group intelligence. A design must be organized according to the structure of communication within the organization like convey law states.
- Data systems amplify the data they hold whereas microservices conceal the data they hold.
- Beware the cycle of data inadequacy, this is why a real data system is needed.
- Throttling is the more targeted version of circuit breaking
- Architects often fall into a trap—a trap that hinges on their fear of duplication.
- Architecture is not the goal. You don’t “win” by having an architecture.
- Big-bang migrations never end well.
- Domain-driven design
- Programming
- References
Early feedback on a concrete implementation is a great way to discover gaps in your knowledge. Try to solve the problem quickly taking shortcuts. Once you know the way, you’ll be able to make steady and incremental progress as an improvement to what you already had. This way you’ll be able to tell whether something is actually worth it or not - how much does it impact the solution that you already have.
If you have a hammer, everything looks like a nail. Then even the simple things might be challenging to get done, since we might unconsciously insist on using the wrong tools, reluctant do admit there is a better solution involving a technique we don’t like. Don’t do it. You must have a clear understanding of what you expect to achieve.
Anything else you do may possibly a waste unless you identify the main problem and figure out the solution for it.
There are three effective motivators:
- Autonomy
-
Urge to direct your own life.
- Mastery
-
The desire to become better and better at something that matters.
- Purpose
-
The need to do something what we do in service to something larger to ourselves.
Mandatory meetings are by definition a contradiction of autonomy. If authority is required to keep people at the meeting, then the meeting is not worthy people’s time. Make people interested, mainly by combining the three motivators - autonomy, mastery and purpose.
Most team leaders grit their teeth, avert their eyes, and just hope that the low performer either magically improves or just goes away. Yet it is extremely rare that this person does either. While the leader is hoping and the low performer isn’t improving (or leaving), high performers on the team waste valuable time pulling the low performer along, and team morale leaks away into the ether. You can be sure that the team knows the low performer is there even if you’re ignoring them—in fact, the team is acutely aware of who the low performers are, because they have to carry them. Ignoring low performers is not only a way to keep new high performers from joining your team, but it’s also a way to encourage existing high performers to leave.
Sometimes people are not willing to take risks and accept responsibility for things that go wrong. But without taking smart risks, there is no progress. If someone’s blocking your progress it is sometimes better to just do it, having a back-out plan if it doesn’t work.
You have to design the system so that when a failure occurs, and it will, the system will successfuly degrade gracefully and then recover quickly.
The ideal test is unchanging: after it’s written, it never needs to change unless the requirements of the system under test change.
-
Test the public API and not the implementation details.
-
Test the system state and not the interactions.
-
Do not check if something gets called or not.
-
Avoid mocking and prefer using real objects.
-
Descriptive And Meaningful Phrases. A little bit of duplication is OK in tests so long as that duplication makes the test simpler and clearer.
Not only should the focus on testing indirectly lead to better design, but problem decomposition according to divide et regna should focus directly on producing modules that are easy to test.
If you want a “clean” architecture, by all means laminate a printout of an idealized version of the system architecture you might have had, if only you had perfect foresight and limitless funds. Real system architecture is a constantly evolving thing that must adapt as needs and knowledge change. Technical debt is a tool that helps remaining agile.
A good architect pretends that the decision has not been made, and shapes the system such that those decisions can still be deferred or changed for as long as possible. The longer you wait to make those decisions, the more information you have with which to make them properly.
Cohesion applies to the relationship between things inside a boundary, whereas coupling describes the relationship between things across a boundary. There is no absolute best way to organize our code; coupling and cohesion are just one way to articulate the various trade-offs we make around where we group code, and why. All we can strive to do is to find the right balance between these two ideas, one that makes the most sense for your given context and the problems you are currently facing.[1]
Share only what you absolutely have to, and send only the absolute minimum amount of data that you need. A loosely coupled service knows as little as it needs to about the services with which it collaborates. The connections between modules are the assumptions which the modules make about each other.
We tend to be drawn to exciting problems to solve without thinking if these really are problems worth solving. Remember that time cannot expand and by investing in once place we’re loosing in other, potentially more beneficial.
Simple systems allow restrictive assumptions which enable powerful optimizations. The best example is SQL vs NoSQL. NoSQL are so fast and scalable because they sacrificed some of the features and guarantees relational database have.
Simple ideas are actually much more likely to be used because of adoption and being able to grow. Simple ideas must fit inside your brain. MapReduce can be one example.
-
Domain coupling describes a situation in which one microservice needs to interact with another microservice, because the first microservice needs to make use of the functionality that the other microservice provides. This type of interaction is largely unavoidable and is considered to be a loose form of coupling, but make sure to hide as much information as possible.
-
Temporal coupling is a situation in which concepts are bundled together purely because they happen at the same time, that is when one microservice needs another microservice to do something at the same time for the operation to complete. Both services need to be up and available to communicate with each other at the same time for the operation to complete. Temporal coupling isn’t always bad; it’s just something to be aware of. As you have more microservices, with more complex interactions between them, the challenges of temporal coupling can increase to such a point that it becomes more difficult to scale your system and keep it working. One of the ways to avoid temporal coupling is to use some form of asynchronous communication, such as a message broker.[1]
-
Pass-through coupling describes a situation in which one microservice passes data to another microservice purely because the data is needed by some other microservice further downstream. In many ways it’s one of the most problematic forms of implementation coupling, as it implies not only that the caller knows not just that the microservice it is invoking calls yet another microservice, but also that it potentially needs to know how that one-step-removed microservice works. The major issue with pass-through coupling is that a change to the required data downstream can cause a more significant upstream change.
-
Common coupling occurs when two or more microservices make use of a common set of data. A simple and common example of this form of coupling would be multiple microservices making use of the same shared database, but it could also manifest itself in the use of shared memory or a shared filesystem. The main issue with common coupling is that changes to the structure of the data can impact multiple microservices at once. Sources of common coupling are also potential sources of resource contention and central points of failure.
-
Content coupling describes a situation in which an upstream service reaches into the internals of a downstream service and changes its internal state. The most common manifestation of this is an external service accessing another microservice’s database and changing it directly. With common coupling, you understand that you are making use of a shared, external dependency. You know it’s not under your control. With content coupling, the lines of ownership become less clear, and it becomes more difficult for developers to change a system.
However, if the interfaces are too restrictive people will work around them, negating their benefit, so this is a tricky balance to get right.
Constraints enforce elegant design and can give rise to its better properties. Constraints guide the "shape" of an architecture by restricting the universe of choices. The more constraints the more one frees one’s self. If true constraints aren’t discovered on time, the complexity will be where it really shouldn’t, as people need challenges.
In architecture you can or build buildings from smaller buildings. In software you create software from smaller software and there are no boundaries. This is why the simple ideas are so important - they can bubble up to the top level of the design. Best example might be map reduce. Building architecture has natural scale - a human being. It is designed for a human scale and this is why it impresses us. For software there is no such thing. A human brain may be a limit - can we fit the idea in our brain? Having too much stuff in your brains slows you down and makes you prone to errors.
Smallness has virtues we should insist on. We should not assume growth without bounds. Economy of scale so much engrained in us but does not apply in software. It does not get cheaper with size. Look at how many people work on this code, it has to be special!
How much work does the developers do on things accidental as opposed to essential?
In software there is a diseconomy of scale the more people you have the more problems you will get - cargo cult programming.
Circuit breakers are there to save the system from crashing completely and allow fast recovery while cutting of the functionality for all users. Throttling is there to do the same but can be scoped at client level and begins to work before any failures occur. Throttling can have multiple phases which may include some mechanics to indicate to the customer that too many calls are being made - like providing headers with warnings and injecting artificial delays before eventually 429 the traffic. Injecting latency can help level out a load a bit and slow down the consumer.
There are different kinds of duplication. There is true duplication, in which every change to one instance necessitates the same change to every duplicate of that instance. Then there is false or accidental duplication. If two apparently duplicated sections of code evolve along different paths—if they change at different rates, and for different reasons—then they are not true duplicates. Return to them in a few years, and you’ll find that they are very different from each other. For this reason, care must be taken to avoid unifying them. Otherwise, separating them later will be a challenge.
Adopting any architecture should be a conscious decision, one based on rational decision making. For instance, you should be thinking of migrating to a microservice architecture only if you can’t find any easier way to move toward your end goal with your current architecture.
Prematurely decomposing a system into microservices can be costly, especially if you are new to the domain. In many ways, having an existing codebase you want to decompose into microservices is much easier than trying to go to microservices from the beginning for this very reason. Small incremental steps can tell you if you are moving into the right direction, before it’s too late.
Ubiquitous language refers to the idea that we should strive to use the same terms in our code as the users use. The idea is that having a common language between the delivery team and the actual people will make it easier to model the real-world domain and also should improve communication.{building-microservices}
The aggregate is a self-contained state machine that focuses on a single domain concept in our system, with the bounded context representing a collection of associated aggregates, again with an explicit interface to the wider world. Each has an internal-only representation and the external representation we expose.
This shared model like customer can have different meanings in the different bounded contexts and therefore might be called different things. We might be happy to keep the name “customer” in finance, but in the warehouse we might call them a “recipient,” as that is the role they play in that context. We store information about the customer in both locations, but the information is different. If the same piece of information is used in two different domains for different things, it must be duplicated, maybe using a different name, rather than introducing coupling in these domains.
We’re trying to capture business rules in the type system. If we do this properly, invalid situations can’t ever exist in the code and we never need to write unit tests for them. Instead, we have “compile-time” unit tests. We can use visibility modifiers like private constructors as well as smart type hierarchies perfectly matching the domain invariants and so on. Another important benefit of this approach is that it actually documents the domain better.
In other words, we should never allow illegal combinations of state by enforcing that invariant by the type system itself. If we have a type which can have many combinations of fields set and some of them are invalid we most likely need to externalize that implicit choice into an explicit choice of stand-alone types - valid combinations.