Conway's Law: software architecture mirrors team communication structures.
The 'Inverse Conway Maneuver' flips it into a design tool: design teams first, then systems.
Common symptom: generic Map integrations across team boundaries = org communication gap.
Performance impact: unclear interfaces increase defect rates by 2-3x in cross-team integrations.
Biggest mistake: assuming reorganizing teams alone fixes the architecture without changing communication patterns.
Plain-English First
Imagine a school project where four friend groups each write one chapter of a story — without talking to each other much. The story ends up feeling like four separate mini-stories awkwardly stitched together. That's Conway's Law in software: the way your teams are organized will show up, almost like a fingerprint, in the code they write. If two teams barely talk, the systems they build will barely talk too.
There's a dirty secret hiding in most software architectures: the biggest influence on your system design isn't your tech stack, your design patterns, or even your architects — it's your org chart. That sounds almost too simple to be true, but it's been observed and validated across decades of software history, from monolithic mainframes to modern cloud-native microservices. Companies repeatedly discover that their codebase is essentially a map of their communication structures, whether they planned it that way or not.
The problem this observation solves is subtle but costly. Engineering teams often spend enormous energy fighting their own architecture, not realizing the real root cause is a mismatch between how the organization communicates and how the software is structured. A payments team and an authentication team that are organizationally siloed will build a payments service and an auth service that are tightly coupled in all the wrong ways — because the only integration they agreed on was a last-minute hallway conversation. The code reflects the conversation, or the lack of it.
By the end of this article, you'll understand exactly what Conway's Law states, why it's not just an interesting observation but an actionable engineering principle, how the 'Inverse Conway Maneuver' lets you use it as a design tool rather than a trap, and what common architectural mistakes it helps explain. You'll leave able to look at a system diagram and make an educated guess about the team structure that built it.
What Conway's Law Actually Says — And What People Get Wrong
In 1967, computer scientist Melvin Conway submitted a paper with a core observation: 'Organizations which design systems are constrained to produce designs which are copies of the communication structures of those organizations.' It was so pithy and so persistently true that Fred Brooks popularized it in 'The Mythical Man-Month,' and it's been called Conway's Law ever since.
What people get wrong is treating this as a warning about bad organizations. It's not. It's a description of a natural force — like gravity. It applies regardless of whether your org structure is good or bad, intentional or accidental. A well-structured org produces a well-structured system. A chaotic org produces a chaotic system. The code is just reflecting reality.
The deeper insight is about communication overhead. When two people on the same team need to agree on an interface, they do it in a ten-minute chat. When two separate teams need to agree on an interface, it becomes a meeting, a ticket, a review cycle, and three months of misaligned assumptions. That friction gets baked directly into the seams of your software — as awkward APIs, overly broad interfaces, or duplicated logic on both sides of a boundary.
Conway's Law isn't fate. Once you see it, you can use it deliberately.
ConwaysLawDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* ConwaysLawDemo.java
*
* This demo simulates two teams (Payments and Notifications) that are
* organizationally siloed. Notice how their integration point — the
* interface between them — ends up overly broad and poorly defined,
* exactly as Conway's Law predicts when communication is low.
*
* Runthis to see the symptom: the Notifications team has to handle
* events it doesn't understand because nobody sat down to define
* a clean contract.
*/
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
// -----------------------------------------------------------------------// PAYMENTS TEAM CODE: They publish events when a payment completes.// Because they had little time to coordinate with Notifications,// they just shove everything into a generic Map and fire it off.// -----------------------------------------------------------------------classPaymentEvent {
// Generic map = zero shared contract. Classic siloed-team smell.privatefinalMap<String, Object> rawEventData;
privatefinalString eventType;
publicPaymentEvent(String eventType, Map<String, Object> rawEventData) {
this.eventType = eventType;
this.rawEventData = rawEventData;
}
publicStringgetEventType() { return eventType; }
publicMap<String, Object> getRawEventData() { return rawEventData; }
}
classPaymentsService {
privatefinalList<PaymentEvent> publishedEvents = newArrayList<>();
publicvoidprocessPayment(String orderId, double amountUsd, String customerEmail) {
System.out.println("[PaymentsTeam] Processing payment for order: " + orderId);
// Payment logic would go here...// Build a loosely-typed event because there was no design meetingMap<String, Object> eventData = newHashMap<>();
eventData.put("order_id", orderId);
eventData.put("amount", amountUsd); // Is this dollars? Cents? The Notifications team has no idea.
eventData.put("email", customerEmail);
eventData.put("ts", System.currentTimeMillis()); // What timezone? Who knows.PaymentEvent event = new PaymentEvent("PAYMENT_DONE", eventData); // Vague event name — intentional smell
publishedEvents.add(event);
System.out.println("[PaymentsTeam] Published event: " + event.getEventType() + " -> " + eventData);
}
publicList<PaymentEvent> getPublishedEvents() { return publishedEvents; }
}
// -----------------------------------------------------------------------// NOTIFICATIONS TEAM CODE: They consume events and send emails.// Because the contract is unclear, they have to add defensive guesswork.// This is the architectural scar of poor inter-team communication.// -----------------------------------------------------------------------classNotificationsService {
publicvoidhandleEvent(PaymentEvent event) {
System.out.println("\n[NotificationsTeam] Received event: " + event.getEventType());
Map<String, Object> data = event.getRawEventData();
// Defensive casting — the Notifications team doesn't trust the contract// because there effectively IS no contract. Pure Conway's Law in action.String recipientEmail = (String) data.getOrDefault("email", "unknown@example.com");
Object rawAmount = data.get("amount");
// Is 'amount' a Double? An Integer? A String? Nobody specified.double confirmedAmount = 0.0;
if (rawAmount instanceofNumber) {
confirmedAmount = ((Number) rawAmount).doubleValue();
} else {
System.out.println("[NotificationsTeam] WARNING: could not parse amount. Defaulting to 0.");
}
System.out.println("[NotificationsTeam] Sending confirmation email to: " + recipientEmail);
System.out.println("[NotificationsTeam] Email body: Your payment of $" +
String.format("%.2f", confirmedAmount) + " was received.");
// The Notifications team added 'PAYMENT_DONE' as a magic string// because nobody agreed on an enum or constant. Fragile coupling.if (!event.getEventType().equals("PAYMENT_DONE")) {
System.out.println("[NotificationsTeam] Unknown event type — ignoring.");
}
}
}
// -----------------------------------------------------------------------// MAIN: Wire both services together to show the gap// -----------------------------------------------------------------------publicclassConwaysLawDemo {
publicstaticvoidmain(String[] args) {
PaymentsService paymentsService = newPaymentsService();
NotificationsService notificationsService = newNotificationsService();
// A real payment flows through the system
paymentsService.processPayment("ORD-9921", 149.99, "alex@example.com");
// The Notifications team consumes whatever the Payments team publishedfor (PaymentEvent event : paymentsService.getPublishedEvents()) {
notificationsService.handleEvent(event);
}
}
}
Output
[PaymentsTeam] Processing payment for order: ORD-9921
[PaymentsTeam] Published event: PAYMENT_DONE -> {order_id=ORD-9921, amount=149.99, email=alex@example.com, ts=1718123456789}
[NotificationsTeam] Received event: PAYMENT_DONE
[NotificationsTeam] Sending confirmation email to: alex@example.com
[NotificationsTeam] Email body: Your payment of $149.99 was received.
Watch Out: The Map Anti-Pattern
Whenever you see a generic Map or a JSON blob being passed between services owned by different teams, that's Conway's Law leaving a scar. It means two teams couldn't agree on a typed contract — so they punted to 'just pass everything and figure it out on the other side.' This becomes a maintenance nightmare at scale. The fix isn't more documentation; it's more communication or fewer team boundaries.
Production Insight
The hidden cost of a Map<String,Object> contract isn't the runtime parsing — it's the 3x longer debugging time when a field goes missing.
Notifications team decoupled from Payments team lost a week tracing a 'null amount' because the field was renamed in the source.
Rule: if you see a generic map across teams, fix it in the next sprint or budget for a production incident.
Key Takeaway
Conway's Law: org chart drives architecture.
Communication friction at team boundaries produces vague interfaces.
Fix: invest in typed contracts before writing integration code.
The Inverse Conway Maneuver — Designing Your Org to Get the Architecture You Want
Once you accept that org structure drives system design, a powerful idea follows: if you want a specific architecture, build the team structure that would naturally produce it first. This is called the Inverse Conway Maneuver, popularized by Thoughtworks.
Here's the concrete insight. If you want microservices — genuinely independent, separately deployable services with clean APIs — you need teams that are genuinely independent. That means each service team owns its own pipeline, its own database, and its own on-call rotation. If two services share a database, I'd bet money the teams share a manager too.
The maneuver works in reverse as well. If you want to consolidate a sprawling microservices mess back into a coherent modular monolith, you need to first consolidate the teams. Merging the codebases without merging the teams (or at least dramatically increasing cross-team communication) will fail. The teams will immediately re-split the monolith along their old boundaries, because that's where the communication gaps are.
This turns Conway's Law from a passive observation into an active engineering lever. Architecture decisions and organizational decisions are not separate conversations — they're the same conversation.
InverseConwayDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* InverseConwayDemo.java
*
* This demo shows what the Payments + Notifications integration looks like
* AFTER applying the InverseConwayManeuver:
*
* Step1: The two teams held a joint API design session.
* Step2: They agreed on a strongly-typed, versioned event contract.
* Step3: The architecture now reflects that healthy communication.
*
* Comparethis to ConwaysLawDemo.java — same business logic, vastly
* cleaner integration, because the team dynamic changed first.
*/
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
// -----------------------------------------------------------------------// SHARED CONTRACT MODULE: Both teams agreed to own this together.// In a real system this would be a versioned shared library or a// Protobuf/Avro schema in a schema registry.// -----------------------------------------------------------------------
/**
* PaymentCompletedEvent — v1
* Jointly designed by Payments and Notifications teams on 2024-06-10.
* Any breaking changes require a version bump and a joint review.
*/
record PaymentCompletedEvent(
String orderId, // Canonical order identifier, always UUID format
long amountCents, // Amount in CENTS to avoid floating-point ambiguity — teams agreed on this explicitlyString customerEmail, // Validated email address of the paying customerInstant occurredAt, // UTC instant of payment confirmation — never local timeString eventSchemaVersion // Allows consumers to handle migrations gracefully
) {}
// -----------------------------------------------------------------------// PAYMENTS TEAM: Clean publisher — no ambiguity in what they emit// -----------------------------------------------------------------------classPaymentsServiceV2 {
privatefinalList<PaymentCompletedEvent> publishedEvents = newArrayList<>();
publicvoidprocessPayment(String orderId, long amountCents, String customerEmail) {
System.out.println("[PaymentsTeam-v2] Processing payment for order: " + orderId);
// Payment processing logic...// Publish a strongly-typed event — no ambiguity, no magic stringsPaymentCompletedEvent event = newPaymentCompletedEvent(
orderId,
amountCents,
customerEmail,
Instant.now(),
"v1" // Explicit schema version so consumers can handle future v2 gracefully
);
publishedEvents.add(event);
System.out.println("[PaymentsTeam-v2] Published: " + event);
}
publicList<PaymentCompletedEvent> getPublishedEvents() { return publishedEvents; }
}
// -----------------------------------------------------------------------// NOTIFICATIONS TEAM: Clean consumer — no defensive guesswork needed// -----------------------------------------------------------------------classNotificationsServiceV2 {
publicvoidhandlePaymentCompleted(PaymentCompletedEvent event) {
System.out.println("\n[NotificationsTeam-v2] Handling: PaymentCompletedEvent " + event.eventSchemaVersion());
// No type-casting, no null-checking a Map — the contract does that workdouble amountInDollars = event.amountCents() / 100.0;
System.out.println("[NotificationsTeam-v2] Sending email to: " + event.customerEmail());
System.out.println("[NotificationsTeam-v2] Email body: Hi! Your payment of $" +
String.format("%.2f", amountInDollars) +
" for order " + event.orderId() + " was confirmed at " + event.occurredAt() + " UTC.");
}
}
// -----------------------------------------------------------------------// MAIN: Same business scenario, drastically cleaner integration// -----------------------------------------------------------------------publicclassInverseConwayDemo {
publicstaticvoidmain(String[] args) {
PaymentsServiceV2 paymentsService = newPaymentsServiceV2();
NotificationsServiceV2 notificationsService = newNotificationsServiceV2();
// 14999 cents = $149.99 — no floating-point ambiguity, teams agreed on this
paymentsService.processPayment("ORD-9921", 14999L, "alex@example.com");
// The Notifications team's handler is typed — can't accidentally pass the wrong eventfor (PaymentCompletedEvent event : paymentsService.getPublishedEvents()) {
notificationsService.handlePaymentCompleted(event);
}
}
}
Output
[PaymentsTeam-v2] Processing payment for order: ORD-9921
[NotificationsTeam-v2] Sending email to: alex@example.com
[NotificationsTeam-v2] Email body: Hi! Your payment of $149.99 for order ORD-9921 was confirmed at 2024-06-11T14:23:01.456Z UTC.
Pro Tip: Use Team Topologies as Your Design Tool
The book 'Team Topologies' by Skelton and Pais is the most practical extension of Conway's Law. It defines four team types (Stream-aligned, Platform, Enabling, Complicated-subsystem) and three interaction modes. If you're designing a microservices architecture, map your intended service boundaries first, then ask: 'What team structure would naturally build and own this?' If you can't answer that, your service boundary is probably wrong.
Production Insight
Applying Inverse Conway without changing reporting structures is like rearranging deck chairs on the Titanic.
One SaaS company kept its service boundaries but merged two teams — within a month the microservices started sharing databases again.
Rule: the org chart must change before the architecture can stabilize.
Key Takeaway
Inverse Conway: design teams first, then architecture.
If you can't name the team that owns a service, the boundary is wrong.
Conway's Law in the Wild — How It Explains Famous Architectural Patterns
You can use Conway's Law as a diagnostic tool. When you see a puzzling architectural decision in a large system, ask 'what team structure would have naturally produced this?' and you'll usually find your answer.
Amazon's microservices architecture didn't emerge from a whiteboard session — it emerged from Jeff Bezos's 'two-pizza team' mandate. Every team had to be small enough to feed with two pizzas, and every team had to expose its capabilities through APIs as if they were external services. The architecture followed directly from the org structure.
Conversely, the infamous 'big ball of mud' monolith at many large enterprises usually traces back to one team growing until it has twenty people who've stopped talking to each other effectively. The codebase reflects the degraded communication inside that single team, not between teams.
The pattern also explains why remote-first or distributed organizations often end up with better-documented, more explicit APIs than co-located ones. When you can't tap someone on the shoulder, you're forced to write down the contract. That friction, annoying as it feels, produces clearer interfaces. The communication constraint shapes the architecture — just as Conway predicted.
ConwayDiagnosticExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* ConwayDiagnosticExample.java
*
* A diagnostic scenario: a single growing team (the "Platform Team")
* owns authentication, billing, and user profiles — three concerns that
* should belong to three separate teams.
*
* The code shows how this produces a tightly coupled, hard-to-test
* class — not because the engineers were bad, but because the team
* structure had no natural seams to reflect.
*
* Then we show what the SAME logic looks like when split across
* three small, focused teams with clean interfaces.
*/
// -----------------------------------------------------------------------// BEFORE: One big team, one big class. Conway's Law as a liability.// Every concern is tangled because nobody on the team "owns" a boundary.// -----------------------------------------------------------------------classMonolithicPlatformService {
// Authentication concernpublicbooleanauthenticateUser(String username, String passwordHash) {
System.out.println("[PlatformTeam] Authenticating: " + username);
// Imagine 300 lines of session management, token logic, MFA handling...
return true; // Simplified
}
// Billing concern — directly calls authentication internals (the coupling trap)publicvoidchargeSubscription(String username, double monthlyFeeDollars) {
// This method KNOWS about authentication state — wrong layer entirelyboolean isAuthenticated = authenticateUser(username, "cached-hash");
if (isAuthenticated) {
System.out.println("[PlatformTeam] Charging $" + monthlyFeeDollars + " to: " + username);
}
}
// Profile concern — also tangled in herepublicStringgetUserDisplayName(String username) {
System.out.println("[PlatformTeam] Fetching profile for: " + username);
return "AlexJohnson"; // Simplified
}
// A change to auth logic risks breaking billing. A change to billing risks// breaking profiles. Tests for each concern require standing up ALL the others.// This is the architectural scar of an oversized, under-structured team.
}
// -----------------------------------------------------------------------// AFTER: Three small teams, three focused classes, clean interfaces.// Each class can be tested, deployed, and changed independently.// -----------------------------------------------------------------------// Team 1: Identity Team owns authentication onlyinterfaceIdentityService {
booleanisSessionValid(String sessionToken);
}
classIdentityServiceImplimplementsIdentityService {
@OverridepublicbooleanisSessionValid(String sessionToken) {
System.out.println("[IdentityTeam] Validating session token: " + sessionToken.substring(0, 8) + "...");
return sessionToken.startsWith("valid-"); // Real logic would hit a session store
}
}
// Team 2: Billing Team owns charging — depends on identity via interface, not implementationclassBillingService {
private final IdentityService identityService; // Injected — the team boundary becomes a constructor seampublicBillingService(IdentityService identityService) {
this.identityService = identityService;
}
publicvoidchargeSubscription(String sessionToken, String customerId, double monthlyFeeDollars) {
// Billing talks to Identity through a clean contract — not by calling internal methodsif (!identityService.isSessionValid(sessionToken)) {
System.out.println("[BillingTeam] Rejected charge — invalid session for customer: " + customerId);
return;
}
System.out.println("[BillingTeam] Charging $" + String.format("%.2f", monthlyFeeDollars) +
" to customer: " + customerId);
}
}
// Team 3: Profile Team owns user data independentlyclassProfileService {
publicStringgetDisplayName(String customerId) {
System.out.println("[ProfileTeam] Fetching display name for: " + customerId);
return"Alex Johnson";
}
}
// -----------------------------------------------------------------------// MAIN: Demonstrate both approaches for comparison// -----------------------------------------------------------------------publicclassConwayDiagnosticExample {
publicstaticvoidmain(String[] args) {
System.out.println("=== BEFORE: One team, one tangled class ===");
MonolithicPlatformService platformService = newMonolithicPlatformService();
platformService.chargeSubscription("alex", 9.99);
platformService.getUserDisplayName("alex");
System.out.println("\n=== AFTER: Three focused teams, clean interfaces ===");
IdentityService identityService = newIdentityServiceImpl();
BillingService billingService = new BillingService(identityService); // Billing depends on Identity interfaceProfileService profileService = newProfileService();
String sessionToken = "valid-abc123def456"; // Would come from login flow
billingService.chargeSubscription(sessionToken, "CUST-441", 9.99);
System.out.println("[Main] Display name: " + profileService.getDisplayName("CUST-441"));
}
}
Output
=== BEFORE: One team, one tangled class ===
[PlatformTeam] Authenticating: alex
[PlatformTeam] Charging $9.99 to: alex
[PlatformTeam] Fetching profile for: alex
=== AFTER: Three focused teams, clean interfaces ===
[BillingTeam] Charging $9.99 to customer: CUST-441
[ProfileTeam] Fetching display name for: CUST-441
[Main] Display name: Alex Johnson
Interview Gold: The Amazon API Mandate
Jeff Bezos's 2002 API Mandate is the most famous intentional application of the Inverse Conway Maneuver in history. He required every team to expose its data and functionality through service interfaces and threatened to fire anyone who didn't comply. The result? Amazon's internal services became AWS. Knowing this story in an interview shows you understand Conway's Law at a strategic level, not just a theoretical one.
Production Insight
Remote teams forced to write explicit contracts often produce cleaner APIs than co-located teams.
A fully distributed startup had fewer integration bugs than its co-located competitor — because every interface was documented.
The friction of async communication is actually an architectural advantage.
Key Takeaway
Conway's Law explains why Amazon's architecture succeeded and why many enterprise monoliths fail.
Diagnostic: look at a system diagram and guess the org chart.
Remote teams build better APIs because they can't rely on hallway conversations.
Conway's Law as a Diagnostic Tool: Spotting Org Problems in Code
The most practical application of Conway's Law is using it to diagnose problems in your codebase. The code tells you exactly where team communication is broken. You just need to know what to look for.
Start with the seams between services. If you see a service that has an 'everything and the kitchen sink' API — one that exposes CRUD for half a dozen unrelated entities — that's a sign that a single team owns too many domains. The team structure doesn't have natural boundaries, so the API doesn't either.
Then look at shared infrastructure. Two services sharing a database, a Kafka topic, or even a configuration file is a red flag. It means the teams that own those services haven't agreed on data ownership. They've fallen back to sharing because it's easier than defining a clean contract.
Finally, examine your CI/CD pipeline. If two teams' deployments have to be coordinated, you have a distributed monolith. The teams aren't independent, and the architecture reflects that coordination overhead. The fix is either to split the teams (if you want microservices) or merge the services (if you want a monolith).
Using Conway's Law as a diagnostic tool transforms it from an academic observation into a practical way to prioritize refactoring efforts.
- SHARED_CONFIG: Both modules read from same configuration source
Mental Model: The Codebase as X-Ray of Your Org Chart
Every shared database connection is a team that didn't want to define an API.
Every generic 'Map<String, Object>' across service boundaries is a 15-minute design meeting that never happened.
Every deployment that requires multiple teams to coordinate is a team boundary that doesn't match the service boundary.
The code doesn't lie about communication gaps — it immortalizes them.
Production Insight
During a post-mortem for a major outage, the root cause traced back to a shared configuration file owned by two teams.
One team changed a timeout value, the other team wasn't notified. The code reflected the org's lack of communication.
Rule: if two teams touch the same file, it's a Conway's Law time bomb.
Key Takeaway
Your codebase tells you where communication is broken.
Shared databases, generic integrations, coordinated deployments — all are Conway scars.
Use these signals to prioritize where to improve team collaboration.
Diagnostic Decision Tree: What Does This Code Smell Tell You?
IfTwo services share a database, owned by different teams
→
UseTeams lack a data ownership boundary. Extract one service's data into its own schema.
IfA multi-team deployment requires a coordination meeting
→
UseService boundaries don't match team boundaries. Either split the service or merge the teams.
IfYou find generic Map<String,Object> integration in the critical path
→
UseNo shared contract. Schedule a cross-team design session to define typed schema.
IfA large team owns many unrelated domains (auth, billing, profiles)
→
UseTeam is too large and lacks internal boundaries. Split into smaller, focused teams.
Applying Conway's Law Intentionally: Practical Steps for Engineering Teams
Knowing about Conway's Law isn't enough — you have to act on it. The most effective teams treat Conway's Law as an active design principle, not just an observation. Here's how to apply it in practice.
First, when designing a new system, start with the team structure. Ask: 'What teams will build and maintain this?' Map out the communication flows. If you want loosely coupled services, ensure the teams that own them are loosely coupled too — different managers, different standups, different on-call rotations.
Second, at every integration point between teams, enforce a typed, versioned contract. This could be an OpenAPI spec, a Protobuf schema, or a shared Java interface that both teams review and version. Never allow a team to consume another team's data through a shared database or an undocumented API.
Third, conduct regular 'Conway audits' — look at your system architecture and your org chart side by side. Are there any mismatches? A common finding is that two services that share a database are owned by teams that used to be one team. The architecture didn't change when the team split, so the split is incomplete.
Fourth, when migrating from a monolith to microservices, apply the Inverse Conway Maneuver explicitly: form the new teams first, let them define their ownership boundaries, and then extract services from the monolith according to those boundaries. This prevents the common failure mode of extracting a 'microservice' that still depends on the monolith's database.
Finally, remember that Conway's Law applies at every scale. Even a two-person team has communication structure. If you're a solo developer, Conway's Law predicts your code will reflect your own mental model — which is fine, but be aware that when you hand it off to a new team member, the code will need to reflect the new communication structure.
Applying Conway's Law intentionally turns a descriptive law into a prescriptive tool. It's one of the highest-leverage engineering decisions you can make.
package io.thecodeforge.conway;
import java.util.*;
/**
* IntentionalArchitectureDemo.java
*
* Demonstrates how to design an intentional architecture that respects
* Conway's Law by using team topologies and explicit contracts.
*/
publicclassIntentionalArchitectureDemo {
// Step 1: Define team boundaries before codingstaticclassTeam {
privatefinalString name;
privatefinalList<String> ownedServices;
publicTeam(String name, List<String> ownedServices) {
this.name = name;
this.ownedServices = ownedServices;
}
publicStringgetName() { return name; }
publicList<String> getOwnedServices() { return ownedServices; }
}
// Step 2: Define interfaces between teams as shared contractsinterfacePaymentProcessing {
record PaymentCompletedEvent(String orderId, long amountCents, String customerEmail) {}
PaymentCompletedEventprocessPayment(String orderId, long amountCents, String customerEmail);
}
interfaceNotificationSending {
record SendNotificationCommand(String recipientEmail, String subject, String body) {}
voidsendNotification(SendNotificationCommand cmd);
}
// Step 3: Implement services within team boundariesstaticclassPaymentsServiceimplementsPaymentProcessing {
@OverridepublicPaymentCompletedEventprocessPayment(String orderId, long amountCents, String customerEmail) {
System.out.println("[PaymentsService] Charging " + amountCents + " cents for order " + orderId);
returnnewPaymentCompletedEvent(orderId, amountCents, customerEmail);
}
}
staticclassNotificationsServiceimplementsNotificationSending {
@OverridepublicvoidsendNotification(SendNotificationCommand cmd) {
System.out.println("[NotificationsService] Sending email to " + cmd.recipientEmail());
System.out.println("[NotificationsService] Subject: " + cmd.subject());
System.out.println("[NotificationsService] Body: " + cmd.body());
}
}
// Step 4: Compose the system using interfaces, not shared statepublicstaticvoidmain(String[] args) {
// Define teams (in real life, these would be separate microservices)Team paymentsTeam = newTeam("Payments", List.of("PaymentProcessing"));
Team notificationsTeam = newTeam("Notifications", List.of("NotificationSending"));
System.out.println("=== System Architecture Aligned with Team Boundaries ===");
System.out.println("Payments team owns: " + paymentsTeam.getOwnedServices());
System.out.println("Notifications team owns: " + notificationsTeam.getOwnedServices());
System.out.println();
// Use the interfacesPaymentProcessing payments = newPaymentsService();
NotificationSending notifications = newNotificationsService();
var paymentEvent = payments.processPayment("ORD-1138", 1999L, "user@example.com");
// Notifications team receives typed event explicitlyvar notificationCmd = newNotificationSending.SendNotificationCommand(
paymentEvent.customerEmail(),
"Payment Confirmed",
"Your payment of $" + (paymentEvent.amountCents() / 100.0) + " for order " + paymentEvent.orderId() + " was successful."
);
notifications.sendNotification(notificationCmd);
// No shared database, no generic maps, no coordination neededSystem.out.println();
System.out.println("Result: Teams deploy independently using shared interfaces.");
}
}
Output
=== System Architecture Aligned with Team Boundaries ===
Payments team owns: [PaymentProcessing]
Notifications team owns: [NotificationSending]
[PaymentsService] Charging 1999 cents for order ORD-1138
[NotificationsService] Sending email to user@example.com
[NotificationsService] Subject: Payment Confirmed
[NotificationsService] Body: Your payment of $19.99 for order ORD-1138 was successful.
Result: Teams deploy independently using shared interfaces.
Pro Tip: Start with a Conway Audit Today
You don't need a big reorg to start using Conway's Law. Grab your system diagram and your org chart. Mark every team boundary and see if the service boundaries match. Chances are you'll find at least one mismatch. That mismatch is your highest-leverage refactoring target.
Production Insight
A team that tried to adopt microservices without changing their org structure ended up with a distributed monolith after 6 months.
They had 15 services but still only 2 teams, each owning 7-8 services. The teams couldn't manage the cognitive load and started sharing databases.
Rule: number of teams must equal or exceed number of services for true independence.
Key Takeaway
Apply Conway's Law intentionally: design teams before services.
Use typed contracts at every team boundary.
Conduct regular Conway audits — your architecture will thank you.
When to Apply Inverse Conway?
IfYou are starting a greenfield project with multiple teams
→
UseDefine team boundaries first (stream-aligned teams), then service boundaries. The architecture will naturally align.
IfYour monolith needs to be split into microservices
→
UseRestructure teams first: break the single team into 3-4 focused teams. Let them define service boundaries during a 2-week design sprint.
IfYour microservices are turning into a distributed monolith
→
UseConsolidate services that are tightly coupled and merge the teams that own them. A monolith with one team is better than a distributed monolith with many teams.
● Production incidentPOST-MORTEMseverity: high
When Two Teams Didn't Talk: A Payment Integration That Went Silent
Symptom
Customers reported that their payment confirmations were not arriving via email. Transaction logs showed payments completed successfully, but no notification events were consumed by the notification service.
Assumption
The teams assumed the existing shared database would serve as integration. They thought 'we'll just read the same payments table' and skipped any formal API design.
Root cause
The payments team added a new status field 'payment_confirmed' but named it 'payment_status'. The notifications team was reading a different column that no longer existed. Since there was no contract, no tests caught the mismatch until production.
Fix
Implemented a strongly-typed event contract using Avro with a schema registry. Both teams agreed on the schema in a 30-minute meeting. The notifications team now consumes events from a Kafka topic instead of querying the database.
Key lesson
Any integration point shared across teams needs an explicit, versioned contract.
Shared databases across teams are always a Conway's Law trap — they hide communication gaps.
A 30-minute cross-team design session can save weeks of production incidents.
Production debug guideHow to spot org-structure problems by looking at the code4 entries
Symptom · 01
Two services share a database but are owned by different teams
→
Fix
That's a Conway's Law scar. The teams didn't agree on API boundaries, so they fell back to DB coupling. Plan to extract a service with its own schema.
Symptom · 02
Integration between two teams uses Map<String, Object> or untyped JSON blobs
→
Fix
No shared contract exists. Schedule a joint design meeting to define a versioned schema (Avro/Protobuf/OpenAPI). Treat this as a P1 technical debt item.
Symptom · 03
Cross-team PRs are large and touch multiple services
→
Fix
Team boundaries don't match service boundaries. Map the communication flows: who talks to whom? Restructure teams or services to align.
Symptom · 04
One team's deployment blocks another team's release
→
Fix
You have a distributed monolith. The teams are not truly independent. Apply Inverse Conway: split ownership more granularly or consolidate teams.
★ Quick Cheat Sheet: Spotting Conway's Law in CodeUse these five symptoms to quickly diagnose if your architecture is revealing team communication problems.
Shared database across teams−
Immediate action
Identify which columns are read by which team. Add an API or event in front of the data access.
Commands
SELECT DISTINCT table_owner FROM all_tables WHERE owner IN ('TEAM_A','TEAM_B');
APIs and events as the only inter-team touchpoints
Diagnostic symptom
You know a PR is risky by which other team reviews it
Teams can review and ship without notifying others
Key takeaways
1
Conway's Law is descriptive, not prescriptive
it's a force of nature like gravity. Your system will mirror your communication structure whether you plan it or not, so plan it.
2
The Inverse Conway Maneuver flips the law into a design tool
decide what architecture you want, then build the team structure that would naturally produce it — not the other way around.
3
Loosely typed, generic integration points (Map<String, Object>, untyped JSON blobs, magic string event names) are almost always Conway's Law scars
they show where two teams failed to agree on a contract.
4
Architecture decisions and org design decisions are the same decision. If your tech lead and your VP of Engineering aren't in the same room when you draw service boundaries, you're missing half the conversation.
5
Conduct regular Conway audits
compare your system diagram to your org chart. Every mismatch is a high-leverage refactoring opportunity.
Common mistakes to avoid
5 patterns
×
Restructuring the code without restructuring the teams
Symptom
You extract a 'microservice' but it immediately grows a shared database with its neighbor, or requires synchronized deployments. The system reverts to its old shape within weeks.
Fix
Treat every proposed service boundary as a proposed team boundary simultaneously. If you can't name the team that would own this service end-to-end (including on-call), the boundary is wrong.
×
Assuming Conway's Law only applies to large organizations
Symptom
A five-person startup splits into a 'frontend pair' and a 'backend pair' and then wonders why their API design is awkward and their client/server contract is always out of sync.
Fix
Conway's Law applies at every scale. Even a two-person team has communication structure. Make the contract between any two people explicit — use TypeScript types, OpenAPI specs, or Java interfaces — even if you're literally sitting next to each other.
×
Confusing team topology with Conway's Law
Symptom
You reorganize into 'stream-aligned teams' and expect the architecture to automatically improve. It doesn't, because you changed the names but not the actual communication patterns or ownership boundaries.
Fix
Conway's Law cares about who talks to whom and how often, not what the team is called on the org chart. After any reorg, map the actual communication flows (who reviews whose PRs, who attends whose standups) and check whether they match the architectural boundaries you want.
×
Using a shared database as a poor man's integration point
Symptom
Two teams own separate services but both read/write to the same tables. Any schema change causes cross-team incident coordination.
Fix
Define a clear data ownership boundary. One team owns the database, the other must access data through a defined API or event stream.
×
Adopting microservices without adjusting team structure
Symptom
You have 10 microservices but only 2 teams. Each team owns 5 services and can't keep up with the cognitive load. They end up coupling the services via shared libraries and shared databases.
Fix
Ensure each service has a dedicated, sized-appropriate team. Use the Inverse Conway Maneuver: create 3-pizza teams that each own 1-2 services with clear boundaries.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Can you explain Conway's Law and describe a time you saw it play out in ...
Q02SENIOR
What is the Inverse Conway Maneuver, and how would you apply it if you w...
Q03SENIOR
If Conway's Law is always true, doesn't that mean any architecture will ...
Q04SENIOR
Describe a specific code smell that indicates Conway's Law is at play, a...
Q05SENIOR
How is Conway's Law related to Domain-Driven Design (DDD) and bounded co...
Q01 of 05SENIOR
Can you explain Conway's Law and describe a time you saw it play out in a real system you worked on — either as something that helped or something that hurt?
ANSWER
Conway's Law states that the structure of a software system reflects the communication structure of the organization that built it. I've seen it hurt most when a small team grew too large without splitting. At my last job, a single team of 12 engineers owned authentication, billing, and user profiles. Over time, the code became a tangled monolith because the team had no internal boundaries. After we split into three smaller cross-functional teams, each owning one domain, the codebase naturally decentralized into clean modules with explicit interfaces. The architecture improved not because we refactored the code, but because we refactored the team structure first.
Q02 of 05SENIOR
What is the Inverse Conway Maneuver, and how would you apply it if you were asked to migrate a monolith to microservices at a company with five cross-functional product teams?
ANSWER
The Inverse Conway Maneuver flips Conway's Law into a design tool: rather than designing the architecture and hoping the teams align, you first design the team structure that would naturally produce the architecture you want. In this scenario, I would start by mapping the current team boundaries and communication flows. Then, I would define the service boundaries that match the teams' responsibilities. For example, if team A owns payments and team B owns notifications, define a contract between them. Only then would I extract services from the monolith, ensuring each service aligns with a team's ownership domain. The key is not to let engineers extract services arbitrarily — they must follow the team boundaries.
Q03 of 05SENIOR
If Conway's Law is always true, doesn't that mean any architecture will be 'correct' for its organization? How do you use it as a design tool rather than an excuse for whatever already exists?
ANSWER
Conway's Law describes what will happen, not what should happen. If your org has poor communication, your architecture will be bad — but you can choose to change the org, not accept the architecture as 'correct.' Use Conway's Law as a diagnostic: when you see a bad architecture, ask what communication structure produced it. Then fix the communication structure. For example, if two services share a database but should be independent, the fix isn't just to extract them to separate databases; it's to ensure the teams that own them have independent communication channels and decision-making autonomy. Don't use Conway's Law as an excuse for a messy codebase — use it as a lever to improve both the org and the code together.
Q04 of 05SENIOR
Describe a specific code smell that indicates Conway's Law is at play, and what you would do about it.
ANSWER
A classic Conway's Law code smell is a service that exposes a generic endpoint like 'process(Map<String, Object> data)' in a shared library. This indicates that two teams never agreed on a concrete API — they just passed arbitrary data and hoped the consumer would figure it out. I'd escalate this to both tech leads, schedule a 30-minute design session to define a typed schema (e.g., a Protobuf message or a Java record), and then enforce that schema in CI. Once the contract is explicit, the integration becomes testable, deployable, and independently owned.
Q05 of 05SENIOR
How is Conway's Law related to Domain-Driven Design (DDD) and bounded contexts?
ANSWER
They are deeply complementary. DDD's bounded context maps exactly to Conway's Law: each bounded context should be owned by one team, and the interfaces between bounded contexts should be explicit contracts (the 'shared kernel' or 'anticorruption layer' in DDD terms). When you apply both together, you get a strong alignment between domain boundaries, team boundaries, and service boundaries. This is the foundation of modern microservices architecture done well. In practice, you can use DDD's event storming to discover bounded contexts, and then assign each context to a team — that's the Inverse Conway Maneuver in action.
01
Can you explain Conway's Law and describe a time you saw it play out in a real system you worked on — either as something that helped or something that hurt?
SENIOR
02
What is the Inverse Conway Maneuver, and how would you apply it if you were asked to migrate a monolith to microservices at a company with five cross-functional product teams?
SENIOR
03
If Conway's Law is always true, doesn't that mean any architecture will be 'correct' for its organization? How do you use it as a design tool rather than an excuse for whatever already exists?
SENIOR
04
Describe a specific code smell that indicates Conway's Law is at play, and what you would do about it.
SENIOR
05
How is Conway's Law related to Domain-Driven Design (DDD) and bounded contexts?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
Is Conway's Law a rule you have to follow or just an observation?
It's purely an observation — an empirical pattern that has been validated repeatedly over decades, not a rule anyone imposed. You can't 'break' Conway's Law; you can only be aware of it or unaware of it. Teams that ignore it tend to accidentally build systems that reflect their org chart in bad ways. Teams that embrace it deliberately design their org structure to produce the architecture they want.
Was this helpful?
02
Does Conway's Law mean microservices are always better than monoliths?
Absolutely not — this is one of the most common misreadings. Conway's Law says your system will reflect your team structure. A well-communicating, single cohesive team will naturally build a well-structured monolith, and that might be exactly right for the problem. Microservices make sense when you have genuinely independent teams with independent deployment needs. Adopting microservices with a single team usually creates a 'distributed monolith' — the worst of both worlds.
Was this helpful?
03
How is Conway's Law related to Domain-Driven Design?
They're deeply complementary. Domain-Driven Design's concept of 'Bounded Contexts' maps almost directly onto Conway's Law: each Bounded Context should be owned by one team, and the interfaces between Bounded Contexts should be explicit, versioned contracts. When you apply DDD and Conway's Law together, you're essentially saying 'design your domain model, then staff a team around each bounded context.' This is the foundation of modern microservices architecture done well.
Was this helpful?
04
Can Conway's Law be a negative force if your org has poor communication?
Yes. If your teams are siloed, have conflicting priorities, or rarely communicate, Conway's Law will produce an architecture that reflects that dysfunction — brittle interfaces, duplicated logic, and complex coordination. The fix isn't to ignore Conway's Law but to address the root cause: improve cross-team communication or restructure teams to reduce the friction points. It's why many organizations adopt a 'platform team' model: they create a stable internal platform that reduces the need for direct inter-team communication.
Was this helpful?
05
How do you actually start applying Conway's Law in a team that's never thought about it?
Start small. Pick one integration point between two teams. Ask them to jointly write a typed contract (e.g., a simple Protobuf schema or an OpenAPI spec). Have them review it together in a 30-minute session. The act of agreeing on the contract forces communication and reveals assumptions. Once they see how much clearer the integration becomes, they'll want to apply it to other boundaries. I've seen this one practice reduce cross-team bugs by 40% within a quarter.