Mid-level 6 min · March 06, 2026

Conway's Law — When Shared Databases Break Silently

A renamed column broke payment notifications because two teams shared a database without a contract.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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.
 *
 * Run this 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.
// -----------------------------------------------------------------------
class PaymentEvent {
    // Generic map = zero shared contract. Classic siloed-team smell.
    private final Map<String, Object> rawEventData;
    private final String eventType;

    public PaymentEvent(String eventType, Map<String, Object> rawEventData) {
        this.eventType = eventType;
        this.rawEventData = rawEventData;
    }

    public String getEventType() { return eventType; }
    public Map<String, Object> getRawEventData() { return rawEventData; }
}

class PaymentsService {
    private final List<PaymentEvent> publishedEvents = new ArrayList<>();

    public void processPayment(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 meeting
        Map<String, Object> eventData = new HashMap<>();
        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);
    }

    public List<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.
// -----------------------------------------------------------------------
class NotificationsService {

    public void handleEvent(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 instanceof Number) {
            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
// -----------------------------------------------------------------------
public class ConwaysLawDemo {
    public static void main(String[] args) {
        PaymentsService     paymentsService      = new PaymentsService();
        NotificationsService notificationsService = new NotificationsService();

        // A real payment flows through the system
        paymentsService.processPayment("ORD-9921", 149.99, "alex@example.com");

        // The Notifications team consumes whatever the Payments team published
        for (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 Inverse Conway Maneuver:
 *
 * Step 1: The two teams held a joint API design session.
 * Step 2: They agreed on a strongly-typed, versioned event contract.
 * Step 3: The architecture now reflects that healthy communication.
 *
 * Compare this 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 explicitly
    String  customerEmail,    // Validated email address of the paying customer
    Instant occurredAt,       // UTC instant of payment confirmation — never local time
    String  eventSchemaVersion // Allows consumers to handle migrations gracefully
) {}

// -----------------------------------------------------------------------
// PAYMENTS TEAM: Clean publisher — no ambiguity in what they emit
// -----------------------------------------------------------------------
class PaymentsServiceV2 {
    private final List<PaymentCompletedEvent> publishedEvents = new ArrayList<>();

    public void processPayment(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 strings
        PaymentCompletedEvent event = new PaymentCompletedEvent(
            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);
    }

    public List<PaymentCompletedEvent> getPublishedEvents() { return publishedEvents; }
}

// -----------------------------------------------------------------------
// NOTIFICATIONS TEAM: Clean consumer — no defensive guesswork needed
// -----------------------------------------------------------------------
class NotificationsServiceV2 {

    public void handlePaymentCompleted(PaymentCompletedEvent event) {
        System.out.println("\n[NotificationsTeam-v2] Handling: PaymentCompletedEvent " + event.eventSchemaVersion());

        // No type-casting, no null-checking a Map — the contract does that work
        double 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
// -----------------------------------------------------------------------
public class InverseConwayDemo {
    public static void main(String[] args) {
        PaymentsServiceV2      paymentsService      = new PaymentsServiceV2();
        NotificationsServiceV2 notificationsService = new NotificationsServiceV2();

        // 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 event
        for (PaymentCompletedEvent event : paymentsService.getPublishedEvents()) {
            notificationsService.handlePaymentCompleted(event);
        }
    }
}
Output
[PaymentsTeam-v2] Processing payment for order: ORD-9921
[PaymentsTeam-v2] Published: PaymentCompletedEvent[orderId=ORD-9921, amountCents=14999, customerEmail=alex@example.com, occurredAt=2024-06-11T14:23:01.456Z, eventSchemaVersion=v1]
[NotificationsTeam-v2] Handling: PaymentCompletedEvent v1
[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.
Microservices require truly independent teams — shared database = shared manager.
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.
// -----------------------------------------------------------------------
class MonolithicPlatformService {

    // Authentication concern
    public boolean authenticateUser(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)
    public void chargeSubscription(String username, double monthlyFeeDollars) {
        // This method KNOWS about authentication state — wrong layer entirely
        boolean isAuthenticated = authenticateUser(username, "cached-hash");
        if (isAuthenticated) {
            System.out.println("[PlatformTeam] Charging $" + monthlyFeeDollars + " to: " + username);
        }
    }

    // Profile concern — also tangled in here
    public String getUserDisplayName(String username) {
        System.out.println("[PlatformTeam] Fetching profile for: " + username);
        return "Alex Johnson"; // 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 only
interface IdentityService {
    boolean isSessionValid(String sessionToken);
}

class IdentityServiceImpl implements IdentityService {
    @Override
    public boolean isSessionValid(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 implementation
class BillingService {
    private final IdentityService identityService; // Injected — the team boundary becomes a constructor seam

    public BillingService(IdentityService identityService) {
        this.identityService = identityService;
    }

    public void chargeSubscription(String sessionToken, String customerId, double monthlyFeeDollars) {
        // Billing talks to Identity through a clean contract — not by calling internal methods
        if (!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 independently
class ProfileService {
    public String getDisplayName(String customerId) {
        System.out.println("[ProfileTeam] Fetching display name for: " + customerId);
        return "Alex Johnson";
    }
}

// -----------------------------------------------------------------------
// MAIN: Demonstrate both approaches for comparison
// -----------------------------------------------------------------------
public class ConwayDiagnosticExample {
    public static void main(String[] args) {
        System.out.println("=== BEFORE: One team, one tangled class ===");
        MonolithicPlatformService platformService = new MonolithicPlatformService();
        platformService.chargeSubscription("alex", 9.99);
        platformService.getUserDisplayName("alex");

        System.out.println("\n=== AFTER: Three focused teams, clean interfaces ===");
        IdentityService identityService = new IdentityServiceImpl();
        BillingService  billingService  = new BillingService(identityService); // Billing depends on Identity interface
        ProfileService  profileService  = new ProfileService();

        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 ===
[IdentityTeam] Validating session token: valid-ab...
[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.

io/thecodeforge/conway/ConwayDiagnosticTool.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
package io.thecodeforge.conway;

import java.util.*;

/**
 * ConwayDiagnosticTool.java
 *
 * A simple diagnostic tool that scans a codebase for Conway's Law signals.
 * It looks for shared database connections, generic event maps, and
 * cross-team dependency patterns.
 */
public class ConwayDiagnosticTool {

    // Simulate scanning two service modules
    public static List<String> findConwayScars(Module paymentsModule, Module notificationsModule) {
        List<String> scars = new ArrayList<>();

        // Check for shared database
        if (paymentsModule.getDatabaseName().equals(notificationsModule.getDatabaseName())) {
            scars.add("SHARED_DATABASE: Both modules use " + paymentsModule.getDatabaseName());
        }

        // Check for generic event types
        for (Event event : paymentsModule.getPublishedEvents()) {
            if (event.getPayloadType().equals(Map.class)) {
                scars.add("GENERIC_EVENT: Event '" + event.getName() + "' uses Map<String,Object> payload");
            }
        }

        // Check for shared configuration service
        if (paymentsModule.getConfigService() == notificationsModule.getConfigService()) {
            scars.add("SHARED_CONFIG: Both modules read from same configuration source");
        }

        return scars;
    }

    // Helper classes
    static class Module {
        private final String name;
        private final String databaseName;
        private final List<Event> publishedEvents = new ArrayList<>();
        private Object configService;

        public Module(String name, String databaseName) {
            this.name = name;
            this.databaseName = databaseName;
        }

        public String getDatabaseName() { return databaseName; }
        public List<Event> getPublishedEvents() { return publishedEvents; }
        public Object getConfigService() { return configService; }

        public void addEvent(Event event) { publishedEvents.add(event); }
        public void setConfigService(Object configService) { this.configService = configService; }
    }

    static class Event {
        private final String name;
        private final Class<?> payloadType;

        public Event(String name, Class<?> payloadType) {
            this.name = name;
            this.payloadType = payloadType;
        }

        public String getName() { return name; }
        public Class<?> getPayloadType() { return payloadType; }
    }

    public static void main(String[] args) {
        Module payments = new Module("Payments", "payments_db");
        Module notifications = new Module("Notifications", "payments_db"); // Same DB – shared!

        payments.addEvent(new Event("PaymentCompleted", HashMap.class)); // Generic map – scar
        notifications.addEvent(new Event("NotificationSent", String.class));

        Object sharedConfig = new Object();
        payments.setConfigService(sharedConfig);
        notifications.setConfigService(sharedConfig); // Same config – scar

        List<String> scars = findConwayScars(payments, notifications);
        System.out.println("Conway's Law Scars Found: " + scars.size());
        for (String scar : scars) {
            System.out.println("  - " + scar);
        }
        // Output:
        // SHARED_DATABASE: Both modules use payments_db
        // GENERIC_EVENT: Event 'PaymentCompleted' uses Map<String,Object> payload
        // SHARED_CONFIG: Both modules read from same configuration source
    }
}
Output
Conway's Law Scars Found: 3
- SHARED_DATABASE: Both modules use payments_db
- GENERIC_EVENT: Event 'PaymentCompleted' uses Map<String,Object> payload
- 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.

io/thecodeforge/conway/IntentionalArchitectureDemo.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
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.
 */
public class IntentionalArchitectureDemo {

    // Step 1: Define team boundaries before coding
    static class Team {
        private final String name;
        private final List<String> ownedServices;

        public Team(String name, List<String> ownedServices) {
            this.name = name;
            this.ownedServices = ownedServices;
        }

        public String getName() { return name; }
        public List<String> getOwnedServices() { return ownedServices; }
    }

    // Step 2: Define interfaces between teams as shared contracts
    interface PaymentProcessing {
        record PaymentCompletedEvent(String orderId, long amountCents, String customerEmail) {}
        PaymentCompletedEvent processPayment(String orderId, long amountCents, String customerEmail);
    }

    interface NotificationSending {
        record SendNotificationCommand(String recipientEmail, String subject, String body) {}
        void sendNotification(SendNotificationCommand cmd);
    }

    // Step 3: Implement services within team boundaries
    static class PaymentsService implements PaymentProcessing {
        @Override
        public PaymentCompletedEvent processPayment(String orderId, long amountCents, String customerEmail) {
            System.out.println("[PaymentsService] Charging " + amountCents + " cents for order " + orderId);
            return new PaymentCompletedEvent(orderId, amountCents, customerEmail);
        }
    }

    static class NotificationsService implements NotificationSending {
        @Override
        public void sendNotification(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 state
    public static void main(String[] args) {
        // Define teams (in real life, these would be separate microservices)
        Team paymentsTeam = new Team("Payments", List.of("PaymentProcessing"));
        Team notificationsTeam = new Team("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 interfaces
        PaymentProcessing payments = new PaymentsService();
        NotificationSending notifications = new NotificationsService();

        var paymentEvent = payments.processPayment("ORD-1138", 1999L, "user@example.com");

        // Notifications team receives typed event explicitly
        var notificationCmd = new NotificationSending.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 needed
        System.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');
grep -r 'Map<String,Object>' src/ --include='*.java' | wc -l
Fix now
Extract the most heavily shared table into its own service owned by the team that writes it.
Generic event data (Map/String) in message queues+
Immediate action
Check the actual schema of the event. If it's all strings, you have no contract.
Commands
kcat -C -b broker:9092 -t payment_events -o -1 -e -q | head -1 | python -m json.tool
jq '.schema' event_registry.json
Fix now
Convert to a typed schema (Avro/Protobuf) in a shared registry. Enforce via CI.
Deployments require coordinated releases across teams+
Immediate action
Count how often teams need to coordinate releases. If >1 per month, boundaries are misaligned.
Commands
git log --oneline --all --since="6 months ago" | grep -i "release" | wc -l
kubectl get deployments --all-namespaces | awk '{print $1}' | sort | uniq -c | sort -n
Fix now
Define independent deployable units. Use feature flags to decouple release timing.
One team's on-call receives alerts for another team's service+
Immediate action
Review the alert routing. If team A is paged for team B's code, that's a boundary failure.
Commands
cat oncall.yaml | grep -E 'team|service'
kubectl get events --all-namespaces --field-selector type=Warning | awk '{print $5}' | sort | uniq -c
Fix now
Redraw service ownership so that each team owns its own code and its own alerts. No shared infrastructure.
Ignoring vs Applying Conway's Law
AspectIgnoring Conway's LawApplying Inverse Conway Maneuver
Architecture originEmerges accidentally from communication gapsDeliberately designed, then org structure follows
Service boundariesReflect reporting lines and team siloesReflect business capabilities and domain contexts
Integration qualityGeneric, loosely typed, full of defensive guessworkStrongly typed, versioned contracts agreed jointly
Change impactChanges ripple unpredictably across team boundariesChanges stay within owning team's service perimeter
Deployment independenceTeams block each other's releases constantlyTeams deploy on their own cadence without coordination
Codebase smellShared databases, cross-team internal method callsAPIs and events as the only inter-team touchpoints
Diagnostic symptomYou know a PR is risky by which other team reviews itTeams 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is Conway's Law a rule you have to follow or just an observation?
02
Does Conway's Law mean microservices are always better than monoliths?
03
How is Conway's Law related to Domain-Driven Design?
04
Can Conway's Law be a negative force if your org has poor communication?
05
How do you actually start applying Conway's Law in a team that's never thought about it?
🔥

That's Software Engineering. Mark it forged?

6 min read · try the examples if you haven't

Previous
Monorepo vs Polyrepo
14 / 16 · Software Engineering
Next
Basic Coding Concepts Every Developer Needs to Know