Learning Software Architecture
matklad.github.io
matklad.github.io
- Good design is a single idea pervaded throughout.
- More generally, your goal should be to minimize surprise.
- If your system allows it, people will do it.
- Everyone will not just. If your solution starts with "if everyone will just..." then you don't have a solution.
- Isolate the parts of your system that transform data from the ones that use it. Data models outlive code.
- Coupling is the root of most evil.
- Versioning is inevitable.
- Make state explicit.
- Every piece of information should have a single source of truth.
- You should spend more time thinking about naming things correctly.
- If testing is difficult, the design is wrong.
- You will regret every undocumented decision.
- Communication is a tax that you should justify before paying it.
Remember that the job of an engineer at any level is to use rules of thumb to solve problems for which there is incomplete information.
- The ultimate goal of software is to solve the immediate problem at hand. The secondary goal of software is to solve likely future problems with as little work as possible. Any bad design which is better on those goals than a good design is actually a good design.
- Make your interfaces easy to use correctly and hard to misuse. Think of how people unfamiliar to the project will interact with them, make the obvious way be the correct way.
- Correct code should be easy to write; suspicious code should stand out.
- Shift bugs left.
- Fixing a bug class is better than fixing a bug.
- Interfaces are harder to change than implementations. An ugly implementation is ok if it has the correct interface.
- Use comments and documentation to explain why the code is the way it is. If it feels like there's a simpler way to do it, but that simpler way wouldn't actually work due to a constraint some people may be unaware of, document that.
- Don't repeat yourself; when it comes to data. If you store a single fact in multiple places; those places will inevitably get out of sync, and that causes bugs.
- There's a cost to straying off the well-trodden path. Don't be afraid to do so when it's truly worth it, but don't underestimate that cost. Worse (boring) technology is often better technology.
- Think in terms of expected value. Think not "Is this thing worth doing?", but "Is this thing worth doing, compared to the other things we could be doing instead?"
- Even if you think you're smarter than everybody else, intelligence isn't always enough, some problems can't be discovered until they happen. Other people have worked longer on this problem than you have. Learn from their mistakes.
- Friction is the silent killer.
Not really. Today I learned of a story where a vibe coding PM deployed a vercel app, which was in its entirety a single react component that serves a banner, that was embedded via an iframe into an entirely separate web app, whose repo they have full access to, because they needed to get a modal component out the door, but were blocked because they “couldn’t upload the image to s3.” (They could just use the public/ or assets/ directory and have the cdn pick it up, too.)
People do the most insane things when they are tight on time and don’t have the intuition to do a bit of research up front. If you ask them, they’re solving the immediate problem at hand. If you ask anyone else who interacts with their work, they’re disrupting our days/weeks/months/years when we have to step through systems that don’t know any better.
If you have the muscle to plan ahead, even a little bit, which is usually just following the same pattern over and over again, then you are fine.
In this world you are still optimizing for the now problem but you have also solved, to some and ideally a better degree, some future problems for free
Solving the immediate problem should be the primary goal (otherwise you get overengineering), but the secondary goal is still important. It seems like that PM didn't consider the secondary goal at all, he achieved the primary one without considering anything else.
Incidentally, this is a pattern agents often fall into. They're great task finishers, not so great architecture designers.
I find this principle to hold only superficially, and once subjected to scrutiny it breaks down.
There is a reason why Don't Repeat Yourself (DRY) was superceded by Write Everything Twice (WET). There were far too many junior devs swearing nonsense such as how a DRY abstract class that is inherited by two concrete classes is simpler than two classes or even functions that do mostly the same thing. These junior devs rolled out convoluted abstraction layers in name of DRY that would make enterprise code blush. Moreover, these simplistic yet fundamentalist DRY tricks introduced coupling between components that are similar at a superficial level but semantically held no relationship.
Instead of rolling out premature abstractions, code must allow duplications. Copying a class poses a far lower risk than dumping a sea of inheritance with a bunch of abstract classes and interfaces and generics. The job of a software architect is to save developers from themselves, and minimizing code is such a way.
- data migrations are inevitable and should be planned for (corollary of versioning)
- planning is good, sometimes you just have to try things out
- everything costs money. Designing without costs in mind will force hard choices down the line
- Code lives longer than you expect. You forget sooner than you expect. Make a readme/architecture overview/theory of operation document. Put more in it than you think is needed. Check it in with the code.
Cheers
Good reminder for the rest of us to enjoy the journey once in a while.
- programmatically enforce your style with in-house linters and scaffolders. Be highly opinionated
- if you # ignore anything, leave a comment explaining why
- use bdd so your cases are human readable and must update with the code (unlike a stale comment)
- follow the same pattern in all your services (I love hexagonal design for backend). If you break from the design have a good reason for it
- COMPOSE your code. It’s almost always the case that two endpoints or jobs or whatever happen to have a few things in common, but differ wildly. Don’t get DRY, just import those things in each spot and don’t entangle them with each other, it’s too confusing
- scope your interfaces tightly. No optional params unless the domain is actually optional. Make separate jobs or endpoints for different configs. I can’t figure out wtf all your configs were from 4 years ago and neither can you
- code is default testable with DI/DIP
- always run tests running real infra like testcontainers - cement ci/cd as your guardian. Employ every linter you can.
- hammer workarounds with comments in the code
- break all systems into small problems and nothing is challenging technically (though the domain might be!)
- work with less people on your code. It’s easier to stay aligned and follow the same patterns
- don’t expect anyone to figure it out. You need to champion your changes. Do code reviews, hold their hand, show them the way
- clear out mundane items like installfests, local auth, server reloads. Slow dev pisses people off
- finally, and most important, employ an org policy for review. Don’t let shit fall through the cracks unknowingly. It compounds. You have skills now as a quick and dirty to eval and enforce. Use them!
I wonder sometimes if the role of architect in a business might be about having a group/team wide senior person who
- Knows how to architect systems very well and can share that knowledge with the larger group and more junior devs - Is a kind of high level business analyst who can speak "business, stuff that makes money" and "dev, how it's done" to each of those groups effectively
Does Google/Atlassian miss out on things by not having system architects? ... Or can they achieve what's needed by having sensible team rules (so architecture gets followed) and relatively standard reusable architectures (so it's not too hard to adapt to a given business solution).
I'd be genuinely interested to know. My aspiration was to become an architect, but I now wonder if this is the right way to go.
> Communication is a tax that you should justify before paying it.
> Every piece of information should have a single source of truth.
- Do as much as possible on a single system and minimize sharing state.
- Recognize that every system is distributed, it's just a question of how and where.
One of the biggest ills I observe with most modern software systems is that we've gone full tilt towards things like microservices which require synchronizing state across multiple interdependent parts. Regardless of how clean the abstractions or how well contracted the APIs, doing all of that copying and state synchronization is going to result in problems: performance problems, cost problems, and synchronicity problems.
- It depends
No solution I've come across, is a solution for everything, everywhere. It's almost always context dependent, and something that is right in one place, can be utterly wrong in another, and there is no universal truths regarding design and architecture.
The more flexible you can be with "right tool for the right job", the easier time you'll have designing, because you're no longer trying to shoehorn in things that are "right" and "correct".
> - You should spend more time thinking about naming things correctly.
This should almost be on the list twice too, the amount of people who couldn't care less about naming, is so damn high, but if people just cared a tiny bit, it'd solve so much future confusion. Whenever I get pulled into helping a "legacy project" or whatever, establishing a "true vocabulary" based on what people actually call things, is the very first thing to do, because it always uncovers that people been talking about different things the entire time.
Of course, I'd have to back that up with assumptions and goals and such, but I think that's the point. X technology is great at Y but not so great at Z, so if you want to optimize for Z maybe don't use X.
But as a designer of the system you must understand the industry and while you don't need to embrace their terminology and modeling habits fully, you must understand their rationale and how they view the data set. There are some places where we've intentionally simplified complications of the healthcare market to eliminate needless (to us) over definitions and provide a more unified modeling. But these changes took significant comprehension of the problem area to make with confidence.
> You should spend more time thinking about naming things correctly.
On this point in particular. Names never die - that's a lie, occasionally they do, but it takes an extreme amount of effort to enforce a renaming. It really is worth spending a big bulk of time letting SMEs stew with naming proposals to make sure you bases are covered. You can force through a few concepts but your business wing (sales, marketing) will constantly put pressure on you towards industry terminology and force your model to adhere to the current view of the industry. If you decide to break with that that break must be decisive and obvious in intent.
Oh, the single biggest attribute of software to emphasize is maintainability. How much will this cost to build is one question - how much will this cost to run (not just infra but compounding feature requests and code refactoring and maintaining third party software versions etc...) is the far more impactful.
In fact the article itself didn't really seem to be too coherent on software architecture. I think the 4+1 view of software architecture is a good conceptual way to think about things (minus all the UML). It's not a complete picture but it addresses the bigger picture. The book series Pattern-Oriented Software Architecture is pretty good and outlines a number of architectures people tend to land on. One stage Grady Booch was working on a handbook of software architecture, but seems to be kind of dead ish, but he did have a mailing list where he'd go to companies or look at large open source projects and look document big system architectures (and some smaller scale), not sure where you can see this anymore. All of these things are worth diving into if you want to learn about software architecture. You can see these architectures are built for different focuses, things like scale, safety, performance, interoperability, failsafe, etc. i.e. there are very specific goals of an architecture with very real tradeoffs.
You never want to work on a platform with 500k lines of code any of which could mutate variable state at will and the more you can isolate or simplify state usage the better your life will be.
All software is actually a state machine and almost all complexity comes from unmanaged state transitions. Especially if you have errors, concurrency, async actions, and distributed systems.
The most important task any software architect has is defining how you know what is true about the system.
Consider a few examples:
await sendEmail(); await saveUser();
That's obvious right? Well what happens when saveUser fails and some retry sends a duplicate email?
---
If you're relying on a webhook to let system A know when system B has changed things, what happens when that failed and now the two systems are out of sync? This is why event-driven architectures often work well: they externalize state transitions.
---
A classic React problem, you define state like this:
``` const [loading, setLoading] = ... const [error, setError] = ... const [data, setData] = ... const [isRefreshing, setRefreshing] = ... ```
Now there's all sorts of invalid states you can be in. Like "loading true, data true, error true".
**
You should strongly prefer: state machines, lifecycle enums, event logs, immutable events, append-only histories, strict versioning, idempotency keys, and explicit workflow stages.
You should avoid like the plauge: hidden globals, timing assumptions, inferred behavior, synchronization, side effects, boolean explosion, mulitple sources of truth.
Event sourcing, CQRS, Redux, Kafka, workflow engines, finite state machines, transactional outboxes, CRDTs, database normalization they all do the same thing _control where the state lives_.
Otherwise, a very wise list!
If you involve five more, the result could be better in theory, but it will certainly take MUCH more time because of communication overhead.
I thought it meant like keep things as local as possible. Like within the same process, within the same machine, avoid going through the network because staying on the processor is always the fastest, going to ram is next fastest, and if you need to communicate across the network it is always slowest
Performance is and always was something dependent on the domain rules. You don't start architecting for performance before understanding requirements so why make it a pillar of all architecture.
But maybe it’s about discussing an issue with 10 other people from different teams when it’s not necessary.
But we will never now. I guess OP didn’t want to pay the communication tax on expressing this idea clearly :)
Don't save the same state in more than one place.
There's a reason caching (and it's invalidation) is one of the two hardest problems in computer science.
One addition?
* start with a modular monolith
Note: I'm saying microservices CAN be the right answer, just not always and we should over-engineering.
sauce: I work as a product engineer where we use microservices extensively. I have also worked on monoliths. Preferred working on the monoliths overall I think.
A modular monolith still forces you to name ownership, data boundaries and invariants, but without making every mistake a networking/ops problem.
Or domain/IO is cumbersome. Think videogames.
It doesn't matter if nobody is interesting it fixing it, video games have been and almost always are written like shitty single use code, (Even when it's not) so it shouldn't surprise anyone that proper test harnesses aren't available in engine.
1. You're allowed to lie for expediency, but make sure you never delude yourself into believing your own lies.
If you're not limited by some legacy software/database initially then you will have such a large problem scope that you'll create your own legacy software/database internally.
I'll grant that if your problem is really simple and straightforward you can sometimes just build an ideal greenfield solution that's perfect and wonderful - but those problems are rarely the problems that are profitable to solve.
For that, I would recommend the classic texts, such as Software Architecture: Perspectives on an Emerging Discipline (Shaw/Garlan) and really anything you can find by Mary Shaw. Including more recent papers that explore why the field of software architecture did not go the way they foresaw, for example Myths and Mythconceptions: What Does It Mean to Be a Programming Language, Anyhow? or Revisiting Abstractions for Software Architecture and Tools to Support Them
More practically: look at why Unix pipes and filters and REST are successful, and where they fall down and why. Hexagonal architecture is also key.
And a plug for my own contribution, linking software architecture with metaobject protocols as a new foundation for programming languages and programming: Beyond Procedure Calls as Component Glue: Connectors Deserve Metaclass Status. An answer to Mary Shaw's Procedure Calls Are the Assembly Language of Software Interconnection: Connectors Deserve First-Class Status.
Answering the question: if procedure calls are the assembly language, what might a high level language look like? And also maybe that software architecture might have a brighter and more practical future ahead of itself.
I’m not well versed in PLT and SWE tooling, but isn’t that the base concept around lambda calculus, LISP, APL, clojure, TCL,…? You only need a few data structures and types, a small collection of fundamental functions, then you compose them.
If there’s one thing that I like about Lisp is that more complex type are always opaque (especially the ones from FFI). I would love to see CLOS implementation for a C-like language, where when you define a struct, what you get is a standard collection of functions.
For the implementation: yes. Hence "assembly language". The mistake almost universally made is to confuse that with the abstraction mechanism.
She showed that it's necessary.
I showed how to do it.
1. Maintain a large enough project. Not create, but support.
2. Do it for at least couple or few projects.
If project is too small, any architecture works fine. "Large" can be in terms of lines of code, but better in terms of people who ever worked on it, or even better -- teams.
At least two different projects is to have something to compare. I've seen people stuck for decades on one project and not knowing any modern ways to solve a problem.
But often the architects who get promoted because they created, not maintained a project. Especially visible in Google, as you don't get promoted for maintaining anything, only for shipping something new (and better jumping off as soon as possible afterwards).
Counterintuitively, people in the best position to be architects are actually side contractors from head shops, who get invited to maintain an existing project no one from a company is willing to (as they all jumped off where promotions go). First, they have to maintain an architecture, and second they did it on several projects, so can compare. The downside though, is if they bill by the hour, the tend to over-complicate architecture to bill more hours.
Not all chapters are equally good or equally interesting, that's the curse of a multi-author book, and all of them are dated, but I think the book is worth reading nonetheless.
- Junior asks to senior: what did you we use an ORM ?
- senior answers: because it's cleaner.
- junior: ???
I prefer when people are able to define a clear list of objectives: - maintenable;
- performant, scalable;
- efficient;
- resilient;
- observable;
- testable (and tested);
- secured;
- readable for new devs that come on board.
Each criteria balance the others, and the more we add criteria the more it helps to make good choice when we hesitate. It is also meaningful for people outside of the dev team, we can reach an agreement with the customer so he knows what he pays for."maintenable" can be also defined, since a project is mostly in maintenance mode during its lifetime (which means the project is successful, which is good !). The ability to incorporate new features without breaking architecture or even without breaking a single method signature is a good starting point.
Being super careful with abstractions. Here someone wrote something like that: "abstraction often hides how what you want is simple". True, ORM I am looking at you. In most case data should be threaten as first class citizen, it also fosters good collaboration with the DBA.
Thinking beyond "the happy path" without falling in premature optimization is also a challenge. A nice one, once again to it avoids to go head first in implementing a good idea without considering drawbacks.
I also tend to imagine that the one that will work on my code had a very bad day so it must be pleasant to read what I wrote. Comments here and there, locale variable here and there even if they can be avoided, variable naming, etc...
Being selective about frameworks. They are good servant but bad leaders. "Be an engineer, not a frameworker" says an article.
When it comes to frameworks - highly opinionated tends to get corrupted into "We have this hammer and suddenly everything looks like a nail" - it doesn't always happen and it doesn't always bite you but when orthodoxies are taken from one domain and applied broadly you run the risk of some of the justifications for that orthodoxy being domain specific and being violated in the wider context.
So, when it comes to frameworks, I like modularity where I have options to plug in a different ORM or persistence integration layer - where I can swap out the router - or the validator - or any other component that proves a poor fit for our problem. It is especially valuable if there are multiple paradigms expressed in alternatives within the framework's ecosystem since you may find a pre-baked tool that mostly works where you can choose an option with shortcomings that are clear and addressable if ever the need arises.
To support maintainability it's very important to fight against NIH-ism, that is a constant danger that can soak up resources at an alarming pace - but it's also important to realize that there are some components that you will greatly benefit from tweaking or taking full ownership of with the most difficult problem being trying to figure out which is which.
I appreciate deeply that your list put maintainability first - that's my opinion as well!
I’m a NP - lots of learning came in clinical rotations where you see real life situations and how they are addressed. I want something like this for software architecture.
The closest I’ve seen is the open source case study books referenced previously but these are older.
I’d like to be able to see explanations at various layers of abstraction about why certain decisions are made or not.
[1]: https://en.wikipedia.org/wiki/SOLID [2]: https://www.youtube.com/watch?v=wo84LFzx5nI
As well as the book (https://leanpub.com/residuality) the author has one on the philosophy of architecture (https://leanpub.com/architectsparadox).
It's heavily dependent on the project, but I feel like working as a "fullstack dev" kind of removes the fun of programming. I'm already spending 40 hrs a week looking at the most dull project I can imagine
Let it go...
Not one single project is free of warts. Not a one. And honestly, if the programming language is that big a deal, you're better off jumping ship. We should all be polyglots, but that's up to you.
A well designed data structure is far more impactful on performance and maintainability than framework, language or platform.
Personally, I work alongside ADHD every day so I need to constantly push to make sure that progress is happening which means picking unimportant decisions and resolving them to focus the remaining open problem space on the decisions that require a lot of careful thought.
You don't have to call a sequence of transformations a compiler. You can say your AST is an algebraic data type, and your transformations are folds (or structural recursions; same thing). Now you have an abstract model that isn't tied to a particular application, and you can more easily find uses for it.
If you know a bit of maths you might wonder about duals. You will find codata---objects---are the dual of algebraic data. Ok, now we're programming to interfaces. That's also useful in the right context. What's the dual of a fold? An unfold! So now we have another way of looking at transformations, from the point of view of what they produce instead of what they consume. At this point we've basically reinvented reactive programming. And on and on it goes.
You can find most of this in the literature, just not usually presented in a compact and easy to understand form.
(Note, the above description is very quick sketch and I'm not expecting anyone to understand all the details from it alone.)
Shameless self promotion: the book I'm writing is about all these concepts. You can find it here: https://functionalprogrammingstrategies.com/
But I would say that just because your preferred mental model is an abstract algebraic one where you build an abstract model that can apply to multiple situations doesn’t mean that such an architecture is best for every situation.
The article talks very clearly about the system and social constraints that it is optimizing for architecturally and ‘turning everything into a fold’ doesn’t immediately strike me as helping to meet the fast-build-feedback needs of the deep contributors and easy-and-safe-to-hack-in-modules needs of the weekend warrriors, which is what are described as the goals of the architecture.
But it also doesn’t strike me as very clearly not the case that the architecture has some of the features you’re describing.
It feels rather like you have a pet mental model which you think all architecture should subscribe to, and… I’m sorry but that seems naive.
I am trying to show 1) software architectures are useful, 2) if you abstract them you can find principles and relationships that allow you to transfer them to different domains, and transform them into different models, and 3) there is a lot of depth in software architecture and utility in learning it.
The article spends most of its time discussing social context in which architecture is developed (I agree it is important, but not everything) and in general downplays the utility of learning about software architecture (e.g. "“software design” is something best learned by doing", and later suggests there is little useful writing on software architecture).
Read tip: Simplify IT - The art and science towards simpler IT solution https://nocomplexity.com/documents/reports/SimplifyIT.pdf
If the agent cannot deduce the constraint, then you have to write it out. Conventions for migration. Invariant properties of the authentication protocol. Multi-tenant behavior. This act of articulation – making your implicit reasoning explicit – led to a kind of clarity that several months of construction alone did not bring. Three architectural errors emerged instantly.
It’s true that learning by doing is the way to go. I would add: learn by explaining it. The agent is an unusually picky audience.
Let me just say: A lot of people think architecture is how do you build a very complex system with tons of moving components (databases, queues, scaling, reduandancy, failover, dozens of services). I think expert achitecture is being able to answer how do I correctly solve the problem using the fewest of those.
Genius-level engineering is inventing the zipper, usable anywhere, lightweight, cheap, sturdy, simple to make.
(I believe he's alluding to this by saying companies follow conways laws and essentially socially construct a lot of work to match up with the number of teams, hence creating a completely unnecessary nightmare of complexity just for the social incentives)
When I was learning and being formally educated I regularly only had like 50 bucks to my name, I couldn't even afford the cheapest VPS I could find. So for me learning the architectural ideas of using AWS services where you have to setup credit card information to even register was not feasible. After all I already had a computer, why not learn how to deploy from there? Especially after reading about horror stories of people racking up huge cloud bills due to some slip up. So AWS was out of the window. Next I got myself a book about Microservices which was popular at the time, but quickly learned that they are about organizational structure rather than software architecture, so I never had a reason to try that architecture.
I still have no idea how to chose the right architecture or make the "correct" decisions on it. I do whatever works and for some reason it still pays my bills. Currently I just use Laravel monoliths for everything and I am pretty sure this is good enough for most web services out there.
I know the original post wasnt about webapps, it's just where I find myself having the most issues.
Yes. At one point I thought a set of good programs can be composed into a good architecture. Then at another stage- with agile, etc.- I was thinking about evolutionary architectures.
But experience and dealing with different kind of problems and systems work best. Also, I have seen Conway's law hold when many other patterns, principles fail.
Books like Garlan Shaw are good but help if you have some experience. Some other books/ courses like Neal Ford's help, but for me those helped only to know the terms/ ideas. Practice and rewrites, tech-upgrades helped more. Also, books like Michael Nygard's Release It.
They are abstract because architecture is for dealing with pain, and you can’t explain pain. You need to experience it and reflect on what was done wrong to be in trouble. The examples are like a gentle pinch which does not prepare you for pulling a muscle in your back or having a nail in your foot.
My strategy is to read the books, try to implement the things at least once to get a sense of the costs, and then get back to it once you’re experiencing pain in your project. It may be too late, but you will then learn the lesson.
We hear a lot about it prior and upon experiencing it I could name it. (Like love, I suppose?)
And when you try to prevent that IoC from leaking into the domain too much, the design often starts to look like hexagonal architecture.
Programming often feels like inventing a new form, but in the end we tend to converge on the shapes that previous programmers already discovered.
nevertheless, I often deceive myself into thinking that I am inventing a new design. In reality, I am usually just being shaped by the IoC model imposed by the framework and by the pressure of business requirements.
Only the scale changes. Similar problems tend to leave similar structures behind.
Sometimes it feels as if earlier generations of programmers have already solved so many of the important problems that all that remains for me is rediscovery.
But I do not want mere rediscovery. I want to create a new kind of problem. Still, in front of the solidity of established engineering, my small mind sometimes feels as if there is no place left for me.
https://github.com/wangjohn/mit-courses/blob/master/6.033/Pr...
I have some experience being a software engineer in a data science team or in a team adjacent to a DS team, a lot of problems that they think are "data science problems" are actually software engineering problems being solved in an extremely convoluted and weird way.
I think that if DS community wasn't so resistant to basic SE good practices, or involved engineers in making architectural decisions, total time and effort needed to solve some problems would be greatly reduced.
When I'm in a bad mood, I even entertain a silly conspiracy theory in my head: they do everything in the most ugly and weird way specifically to keep people like me out :)