Head First Software Architecture
Good software architecture is like a good house blueprint, the structure of how the software is laid out
-needs to be easy to:
--scale
--maintain
--keep reliable
Use diagrams: flow chart from start to end
--building plan, so to speak, has an overall shape and supports stuff like
-scalability
-testability
-availability
-performance
-reliability
-made of compontents that interact with each other and doesn’t kill the whole thing when they need to be swapped/updated: DECOUPLED
things you choose for the system - database type, capabilities, etc. - have trade offs and should be considered in the architecture with what you’re trying to do
--these are STRATEGIC DECISIONS more than tactical
--these decisions also can revolve around the EFFORT needed to change, and thus are more architectural (more planning/more people involved/long term)
REQUIREMENTS => CONSIDERATIONS => DESIGN
RE-ITERATE BASED ON FEEDBACK
--consider the DOMAIN (the problem you want to solve) and FUNCTIONALITY (how you do it)
Start small, go large, layers on layers
-————
when looking at software architecture, gotta consider characteristics => how they impact the architecture
--stuff like: security, scalability, usability, reliability, all the -ilities
--there’s a ton of these: focus on the ones that make the most sense and don’t go overboard to keep complexity down
--so get the right ones from: the problem domain, sort of environment you’re in (startup? slower enterprise?), sort of surrounding domain (building a payment system? helps if you know about the financial surrounding stuff)
--of course getting these from the requirements is part of this
get requirements > analyze architectural characteristics and logical components from them along with other criteria > use that to influence architectural style
=> dig deeper into the problem domain: get specifics
=> environment: priorities of the organization itself
=> keep asking why to drill down to requirements; and look to translate what’s asked into specific and measurable characteristics
=> what’s the surrounding context? (EX: ecommerce site. Scalability/performance higher than security/data integrity (though these are important too))
four parts:
architectural characteristics: nonfuctional requirements
architectural decisions: problem domain
logical components
architectural style: do a trade-off analysis of the above then settle into this
=> try to focus on a limit of seven characteristics
-———
two things you have to remember when making software architecture decisions:
1. every decision has trade-offs so do an analysis and see what you can live with in order to meet top priorities and goals
2. why is more important than how: you need to know WHY you chose particular options
trade-off analysis: from top priorities and goals, look at pros/cons of choice in light of other constraints you might have
record WHY using architectural decision records (ADRs):
title, status, context, decision, consequences, governance, notes
-titles are prefix and description: 000-chose microservices architecture
-status becomes Accepted when all sign off: if any later decision changes this, write an ADR for the new decision and modifiy the old one’s status to Superseded
-———
LOGICAL COMPOMENTS:
(include a sample component breakdown here to help)
System is made of logical components so you need to know how to break them down wisely and set them up in good logical architecture:
Step 1: ID components
Step 2: assign requirements to them
Step 3: make sure component SHOULD handle that requirement
Step 4: make sure component satisfied architectural characteristics
Two ways to ID initial core logical components:
1. map out the customer journey workflow to help ID the major stops on the way
2. figure out the actors in the system and the actions they take, then assign them to components (this can include a background “system” actor)
You need to make them smaller and specific and not stuff like OrderManager - that’s too big and has too much responsibility, BREAK IT DOWN and delegate to other components
Then: when you check components against architectural requirements, you can combine/break down components as needed
=> HANDLING COUPLING
When components depend on other components to do functions, that’s coupling
Afferent: ingoing; B, C, D are dependent on A
Efferent: outgoing; W is dependent on X, Y, Z
A little bit of coupling is inevitable but the goal is to reduce it as much as you can based on tradeoffs and reduce what services/components about each other
--good trick for this, use a distributed workflow (IE stages) so changes have less risk
--sometimes though a centralized workflow with higher coupling is fine
Look at: => A => B => C
-count both ends of the arrow for coupling if they connect between two components
-above has total coupling of 5: +1 to A, +1 from A to B, +1 from B getting A, +1 from B to C, and +1 from C getting B
-———
ARCHITECTURAL STYLES
Concerning architectural styles: it’s going to depend on how the code is divided (technical concerns or domain [AKA busness] concerns); along with how it’s deployed (one unit or multiple?)
TECHNICAL CONCERNS: the layers of the code, each ROLE of the code - presentation, services, etc.
DOMAIN CONCERNS: seperated in ways that align with the problem you’re trying to solve - customer, payment, shipping, etc.
MONOLITH: one big unit of code, built and run as one process: easier and cheaper to build; scaling is way harder and all-or-nothing
DISTRIBUTED: code is deployed as seperate units: great scalability and resiliance, expensive and complex
Remember that these distincts have trade-offs, pros and cons: it’s going to depend on the software, keep it in mind as you’re designing
=> LAYERED
monolith, divided by technical concerns: it’s kind of like Model-View-Controller (seperated by techical concerns) but translated into physical layers; often based around existing design patterns
-> take a user request, track it through each layer, and translate into logical components
-> for MVC, the resulting architecture is often presentation/workflow/persistence to database and back
-> good if you need: simplicity, reusuability, performance, speed to build
-> but: not easy to deploy or scale or test
Typically you can be well off choosing this if your problem domain is smaller and more simple - later you can refactor into a more modular monolith. The thing is that this structure can degrade over time as functionality is added, and if you have to change the problem domain, that means changing around all the layers.
Remember: problem domains typically exist in multiple physical layers, so break it apart based on these layers when considering components
Few types of layered architectures:
* two-tier: presentation/business rules/persistence in one deployment unit that connects to DB
* three-tier: presentation, business rules/persistence, database (IE, web app frontend, backend, database
* embedded/mobile: all of it in a physical deployment including DB\
=> MODULAR MONOLITH
To Build:
- divide main problem domain into subdomains/business concerns (Online Store => Order Placement, Payment, Inventory Management, etc.)
- each subdomain = module = its own layered structure with presentation/business rules/workflow/persistance
- all subdomains usually still talk to a single main monolithic database; you could also take a modular approach to it too, but takes a certain approach
- each one needs an API so modules can get/set to other module APIs without cross talk and to hide implementation of modules from others
- needs upkeep to keep it good and working well
=> MICROKERNEL
-this is basically a core system that implements plugins of functionality: the core usually does VERY LITTLE WORK without a plugin
-the core is ideally supposed to stay pretty much static and help with integration of the plugins: benefits include modularity, security, reliability, and scalability especially for rapid chnage
-they can be monolithic or layered deployments though or even hybrid
core = sync/async call => interface ==> plugin
then
plugin ==> interface ==> core
If two plugins need to talk to each other, do it through the core:
X <==> core <==> Y
This is NOT a good architecture if the core needs to change a lot, or you’ve made the wrong things plugins.
-———-
MICROSERVICE ARCHITECTURE
Group of independent software units deployed seperately but can also work together if needed and this is great if you need scalability, fault tolerance, easy maintaibility and deployability
-each unit does one thing really well and is the only one that directly accesses its data; others need to ask them for relevant information
The trick is knowing how small to make them - too small and the service has high coupling and is less reliable, but too big and it’s harder to test/maintain/scale
=> When to make microservices smaller
to make sure they’re all cohensive, you need to control information access, if the code needs to be fault tolerance or has to change a lot, needs to be scaled
=> When to make microservices bigger
Do it when you need to make more database transacvtions, if there’s a lot of data dependencies, or splitting them apart would have way too much dependent coupling
RULE OF THUMB: start with a bigger, more coarse microservice, then break it down as needed based on what you learn.
Code reuse: you can use a shared service other microservices tap into, or you can create a seperate library that the microservices get on compile, based on use case
=> if you need to tap into multiple microservices at once for a request, set up a workflow. Two ways of doing it:
* ORCHESTRATION: orchestrator service coordinates all services to organize requests, get data from each service, and push back to orchestrator (but of course one singular fail point is coupled)
* CHOREOGRAPHED: each service does thing, then calls next microservice to do thing in order (tradeoff is bad state mangement and error handling, but loose coupling and better scalability)
-———
EVENT-DRIVEN ARCHITECTURE
This lets you work multiple async events at once and resolve them as they go which is great performance and very scalable, but complex
-each step is done by a service working at the same time and talkiing to each other async and doesn’t wait on any one service to finish first
User action => triggers async event => event passes info to other services and broadcasts that something has already happened => this initiating event can kick off other, derived events
Services can listen to events
--but: use sync communication when error handling is more important
Database types for this architecture:
- monolithic: they all tap into the one database, simple but hard to scale and not fault tolerant
- domain-partitioned: each domain has a database, simple and good performance, but service coupling, a little more fault tolerance
- database-per-service: each service has a database, easy to change and scale and very fault tolerance, but super coupled, expensive, performance is slower
HYBRID VERSION: EVENT-DRIVEN MICROSERVICES
=> basically, the overall service is split into multiple single-purpose services, and each of these services trigger their own async events that other services can listen for
-———
DESIGNING AN ARCHITECTURE FROM REQUIREMENTS
-always gather additional info as needed
-use these and technical restraints to pinpoint driving architectural characteristics
-tie them back to requirements or business needs
-avoid adding physical details like databases/services/queues/UIs
-consider the characteristics of the chosen architectural style, the problem domain, and the other driving architectural characteristics
-hybrid styles are fine if you can justify it
-remember to document your choices using architectural decision records
-make sure to use the components you IDed in your logical structure for your physical architecture
-consider the trade-offs all the time
Remember the four dimensions:
architectural characteristics: nonfuctional requirements
architectural decisions: problem domain
logical components
architectural style: do a trade-off analysis of the above then settle into this