Choosing and combining styles
No real system is one style. This last page shows how Pedal's needs change from one city to thirty, how a weighted table of characteristics points to different styles at each stage, where such a table misleads, and how six styles end up combined in one system, each at the level where it pays for itself.
1. In one picture
Choosing a style is choosing which characteristics matter most (simplicity, independent releases, elasticity, fault isolation, ease of change, cost…) and accepting what the style costs elsewhere. The Software architecture track covers the method in depth: identifying characteristics (module 04), trade-off analysis (06), and choosing a style (17). This page shows it on Pedal, at two moments in its life.
Real systems combine styles at different levels: one style for how the system is deployed, another for how each deployable is organised inside, another for how parts talk. The skill is naming which style applies where, and why.
| Level | Question | Styles that answer it |
|---|---|---|
| Blast radius | How much can one failure take down? | Cell-based |
| Deployment | How many things do we release, and who owns them? | Monolith, service-based, microservices, serverless |
| Communication | Do parts wait for each other? | Client-server, broker, event-driven, peer-to-peer |
| Inside one deployable | Where do the rules live? | Layered, hexagonal, clean, microkernel |
| Special paths | Does one flow need its own shape? | Pipes and filters for reports, space-based for surges |
2. Pedal built this way
Pedal at launch is one team in one city: a modular monolith, hexagonal inside, is plenty. Three years later it runs 30 cities with eight teams. Here's the combination that fits that Pedal, level by level.
Notice what didn't happen: Pedal never became microservices everywhere. The monthly reports stayed a pipeline, the pricing service kept its plugins, and the docks' offline gossip only exists for the one situation that needs it. Each style is used where its cost buys something.
3. The rules
| Rule | Why it exists | Broken when… |
|---|---|---|
| Start from characteristics, not styles | The style follows from what must be true, not from fashion. | "We're doing microservices" before anyone asked what must scale or release independently. |
| Name the style at each level | Everyone knows which rules apply where. | Half the team thinks it's a monolith, the other half thinks it's services. |
| Spend complexity only where it pays | Expensive styles go to the parts that need them. | Space-based machinery for the admin dashboard. |
| Record the decision and its triggers | The next team knows why, and when to revisit. | Nobody remembers why there are 40 services. |
4. How it goes wrong
5. The code
A small weighted decision table: our own 1–5 ratings of eight deployment-level styles against six characteristics, scored for three moments in Pedal's life. Change the weights and the ratings, and argue with them.
from __future__ import annotations
CHARS = ["simplicity", "independent releases", "elasticity", "fault isolation", "ease of change", "low cost"]
# Our own 1-5 ratings: a starting point to argue with, not a fact.
RATINGS = {
"modular monolith": [5, 2, 2, 2, 3, 5],
"microkernel": [4, 3, 1, 2, 4, 4],
"service-based": [3, 3, 3, 3, 3, 3],
"microservices": [1, 5, 5, 4, 5, 1],
"event-driven": [2, 4, 4, 4, 4, 2],
"serverless": [3, 4, 5, 3, 3, 4],
"space-based": [1, 2, 5, 3, 2, 1],
"cell-based": [1, 3, 4, 5, 2, 1],
}
def rank(weights: list[int]) -> list[tuple[str, int]]:
scores = {style: sum(w * r for w, r in zip(weights, rs)) for style, rs in RATINGS.items()}
return sorted(scores.items(), key=lambda kv: (-kv[1], kv[0]))
def show(stage: str, weights: list[int]) -> None:
top = rank(weights)[:3]
important = [c for c, w in zip(CHARS, weights) if w >= 4]
print(f"{stage} (cares most about {', '.join(important)}): " + " > ".join(f"{s} {n}" for s, n in top))
if __name__ == "__main__":
show("Pedal at launch, one team", [5, 1, 2, 1, 3, 5])
show("Pedal in 30 cities, 8 teams", [2, 5, 4, 4, 4, 2])
show("Festival surges matter most", [2, 2, 5, 3, 2, 2])
const CHARS = ["simplicity", "independent releases", "elasticity", "fault isolation", "ease of change", "low cost"];
// Our own 1-5 ratings: a starting point to argue with, not a fact.
const RATINGS: [string, number[]][] = [
["modular monolith", [5, 2, 2, 2, 3, 5]],
["microkernel", [4, 3, 1, 2, 4, 4]],
["service-based", [3, 3, 3, 3, 3, 3]],
["microservices", [1, 5, 5, 4, 5, 1]],
["event-driven", [2, 4, 4, 4, 4, 2]],
["serverless", [3, 4, 5, 3, 3, 4]],
["space-based", [1, 2, 5, 3, 2, 1]],
["cell-based", [1, 3, 4, 5, 2, 1]],
];
function rank(weights: number[]): [string, number][] {
const scores: [string, number][] = RATINGS.map(([s, rs]) => [s, rs.reduce((t, r, i) => t + r * weights[i], 0)]);
return scores.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}
function show(stage: string, weights: number[]): void {
const top = rank(weights).slice(0, 3);
const important = CHARS.filter((_, i) => weights[i] >= 4);
console.log(`${stage} (cares most about ${important.join(", ")}): ` + top.map(([s, n]) => `${s} ${n}`).join(" > "));
}
show("Pedal at launch, one team", [5, 1, 2, 1, 3, 5]);
show("Pedal in 30 cities, 8 teams", [2, 5, 4, 4, 4, 2]);
show("Festival surges matter most", [2, 2, 5, 3, 2, 2]);import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
static final List<String> CHARS = List.of("simplicity", "independent releases", "elasticity", "fault isolation", "ease of change", "low cost");
// Our own 1-5 ratings: a starting point to argue with, not a fact.
static final Map<String, int[]> RATINGS = new LinkedHashMap<>();
static {
RATINGS.put("modular monolith", new int[]{5, 2, 2, 2, 3, 5});
RATINGS.put("microkernel", new int[]{4, 3, 1, 2, 4, 4});
RATINGS.put("service-based", new int[]{3, 3, 3, 3, 3, 3});
RATINGS.put("microservices", new int[]{1, 5, 5, 4, 5, 1});
RATINGS.put("event-driven", new int[]{2, 4, 4, 4, 4, 2});
RATINGS.put("serverless", new int[]{3, 4, 5, 3, 3, 4});
RATINGS.put("space-based", new int[]{1, 2, 5, 3, 2, 1});
RATINGS.put("cell-based", new int[]{1, 3, 4, 5, 2, 1});
}
static List<Map.Entry<String, Integer>> rank(int[] weights) {
List<Map.Entry<String, Integer>> scores = new ArrayList<>();
RATINGS.forEach((s, rs) -> { int t = 0; for (int i = 0; i < rs.length; i++) t += rs[i] * weights[i]; scores.add(Map.entry(s, t)); });
scores.sort(Comparator.comparing((Map.Entry<String, Integer> e) -> -e.getValue()).thenComparing(Map.Entry::getKey));
return scores;
}
static void show(String stage, int[] weights) {
List<String> important = new ArrayList<>();
for (int i = 0; i < weights.length; i++) if (weights[i] >= 4) important.add(CHARS.get(i));
String top = rank(weights).subList(0, 3).stream().map(e -> e.getKey() + " " + e.getValue()).collect(Collectors.joining(" > "));
System.out.println(stage + " (cares most about " + String.join(", ", important) + "): " + top);
}
public static void main(String[] args) {
show("Pedal at launch, one team", new int[]{5, 1, 2, 1, 3, 5});
show("Pedal in 30 cities, 8 teams", new int[]{2, 5, 4, 4, 4, 2});
show("Festival surges matter most", new int[]{2, 2, 5, 3, 2, 2});
}
}Output (the same in all three languages)
Pedal at launch, one team (cares most about simplicity, low cost): modular monolith 67 > serverless 61 > microkernel 59 Pedal in 30 cities, 8 teams (cares most about independent releases, elasticity, fault isolation, ease of change): microservices 85 > serverless 78 > event-driven 76 Festival surges matter most (cares most about elasticity): serverless 62 > microservices 61 > event-driven 56
6. Try it
Weigh the characteristics
Set how much each characteristic matters to you, or start from a preset. The bars rank eight deployment-level styles using our own 1–5 ratings (the same as the code above).
The ratings are our judgement for a typical team, and you should argue with them. A table like this compares broad properties; it can't see details such as "hot shared data" or "must work offline", which is exactly where the conversation has to go next.
7. What it costs, and when to pick another style
What you get
- Each part gets the shape it needs: no forcing reports, surges and plugins into one mould.
- Complexity where it pays: simple parts stay simple.
- Evolvable: change the style at one level without redoing the others.
What you pay
- More concepts: the team must understand several styles and where each applies.
- Boundaries between styles need care: events in, calls out, translation at the edges.
- Documentation matters: without a picture and decision records, nobody knows which rules apply where.
| If… | Consider instead |
|---|---|
| A new product, one team | Modular monolith, hexagonal inside |
| Many teams that must ship independently | Microservices with events between |
| Small team, spiky and low traffic | Serverless |
| Many customers needing different behaviour | Microkernel |
| A failure must never reach everyone | Cell-based around the rest |
8. Signs you've outgrown it
- The characteristics you chose for have changed: more teams, more traffic, new rules, new regions.
- Most pain comes from one level (releases, scaling, failures) that the current style doesn't address.
- The decision records no longer match how the system actually works.
9. Remember it and say it
How this hook works: what “Pick for the forces, mix by level” means
What the slogan means, phrase by phrase
- “Pick for the forces”
- Start from what must be true: releases, scaling, isolation, cost.
- “mix by level”
- Deployment, communication and inside-the-code styles are separate choices.
Why this picture fits: A city: a stadium for crowds, houses for families, a hospital that never closes, a market that changes weekly.
The story → the concept
| The city | Pedal at 30 cities |
|---|---|
| Districts | Cells by region |
| Buildings | Rides, docks and billing services |
| The roads | Events between services |
| Rooms inside a building | Hexagonal code inside each service |
Watch it on an example
- At launch: a modular monolith scored highest for one team.
- At 30 cities and 8 teams: microservices with events, inside cells.
- Reports stayed a pipeline; pricing kept its plugins.
The trap it saves you from
Choosing a style because a famous company uses it.
It still works when…
For your own system: name the style at each level and the characteristic it serves, in one sentence each.
In an interview or design review
- Lead with characteristics: "The top three are independent releases for eight teams, elasticity for the morning peak, and fault isolation between cities."
- Then name styles by level: cells by region, microservices inside, events between, hexagonal code inside each service, a pipeline for reports.
- Say what you'd do at launch instead and the triggers for moving on: that shows judgement, not fashion.
- Record it: an ADR per decision (Architecture module 27).
10. Check yourself
What should a style choice start from?
Can one system be microservices, event-driven and hexagonal at once?
A scoring table puts serverless first for festival surges. What should you ask next?
When should a style decision be revisited?
11. Go deeper
| Resource | Why |
|---|---|
| Architecture 17: Choosing and combining styles | The architect's method in depth. |
| Architecture 06: Trade-off analysis | Weighing characteristics properly, beyond a simple table. |
| Architecture 27: Architecture decisions and ADRs | Recording the choice and when to revisit it. |
| The comparison table | Every style building Pedal, side by side. |