What Are the Core Architecture Principles Every Junior Developer Should Know?
When I started my first job as a junior developer, I kept running into the same confusing situations. I’d fix a bug in one module, and something would break somewhere else. I’d write tests that required pulling in half the codebase. I’d spend hours debugging race conditions that seemed impossible to reproduce.
What frustrated me most was not understanding how everything connected together. I knew how to write functions and classes, but the “big picture” of system design felt like a black box.
Then I stumbled upon a Reddit thread where experienced architects shared what they called the “two golden rules” of architecture. These two principles changed how I think about software design.
The Two Rules That Actually Help
Here they are, stripped of all the fancy terminology:
- Data should have one and only one writer
- Dependencies should point from less stable to more stable
That’s it. Two rules. But they solve about 80% of the architecture problems I encounter.
Let me show you why they work and how to apply them.
Rule #1: The Single Writer Principle
The Problem I Kept Hitting
I was building a simple banking system. Multiple parts of the code needed to modify account balances. Something like this:
class BankAccount: def __init__(self, balance=0): self.balance = 0
class ATM: def withdraw(self, account, amount): if account.balance >= amount: account.balance -= amount # Multiple writers!
class OnlineBanking: def withdraw(self, account, amount): if account.balance >= amount: account.balance -= amount # Race condition waiting to happenEverything seemed fine in testing. But in production? Random negative balances. Missing transactions. Customers getting angry.
I tried adding locks. I tried making things synchronized. The code got messier, but the bugs didn’t go away.
The Question That Changed Everything
Then I read this advice from an experienced architect: “Can I get rid of this race condition by serializing the data mutations with a single writer?”
That question made me realize I was solving the wrong problem. I wasn’t fighting concurrency—I was fighting my own design.
The Solution: One Writer to Rule Them All
class AccountWriter: """Single writer - only this class modifies account balance""" def __init__(self): self._pending_withdrawals = []
def request_withdrawal(self, account, amount): # Queue it up, don't modify directly self._pending_withdrawals.append((account, amount))
def process_pending(self): # All modifications happen here, one at a time for account, amount in self._pending_withdrawals: if account.balance >= amount: account.balance -= amount return True return False self._pending_withdrawals.clear()
class BankAccount: def __init__(self, balance=0, writer=None): self.balance = balance self._writer = writer or AccountWriter()
def withdraw(self, amount): # Delegates to single writer - no direct modification return self._writer.request_withdrawal(self, amount)
class ATM: def withdraw(self, account, amount): return account.withdraw(amount) # Goes through single writer
class OnlineBanking: def withdraw(self, account, amount): return account.withdraw(amount) # Goes through single writerNow there’s only one place where balances change. Race conditions? Gone. Debugging? Easy—I know exactly where to look when balances are wrong.
Why This Works
When data has multiple writers, you’re always fighting:
- Race conditions: Two writers trying to modify at the same time
- Debugging nightmares: Who changed this value? Good luck finding out
- Unpredictable state: The system behaves differently depending on timing
When data has one writer:
- No race conditions: Only one thing modifies the data
- Clear ownership: You always know who’s responsible
- Predictable flow: Data mutations follow a single path
Rule #2: Dependencies Point to Stability
The Tight Coupling Problem
After fixing the banking system, I hit another wall. My UserService was directly using PostgreSQLDatabase. When I wanted to switch to MySQL for testing, I had to rewrite half my code.
class UserService: def __init__(self): self.db = PostgreSQLDatabase() # Tight coupling!
def get_user(self, user_id): return self.db.query(f"SELECT * FROM users WHERE id={user_id}")
class PostgreSQLDatabase: def query(self, sql): # PostgreSQL-specific implementation passTesting this was a nightmare. I couldn’t test UserService without spinning up a real PostgreSQL instance. And if I wanted to change databases? Rewrite everything.
The Question That Unlocked It
Another piece of advice from that Reddit thread: “If I insert a stable interface between these two dynamic modules, will that help me test both of them independently?”
This was the dependency inversion principle in action, but explained in a way that actually made sense.
The Solution: Depend on Stable Abstractions
from abc import ABC, abstractmethod
# STABLE: This interface rarely changesclass DatabaseInterface(ABC): @abstractmethod def get_user(self, user_id): pass
# UNSTABLE: Implementation can change anytimeclass PostgreSQLDatabase(DatabaseInterface): def get_user(self, user_id): # PostgreSQL-specific code here pass
class MySQLDatabase(DatabaseInterface): def get_user(self, user_id): # MySQL-specific code here pass
# UNSTABLE: UserService changes, but depends on stable interfaceclass UserService: def __init__(self, db: DatabaseInterface): # Depends on stable abstraction self.db = db
def get_user(self, user_id): return self.db.get_user(user_id)
# Testing is now easydef test_user_service(): mock_db = MockDatabase() # Fake implementation for testing service = UserService(mock_db) # Test UserService without touching any real databaseNow the dependency arrow points from unstable (UserService) to stable (DatabaseInterface). I can swap implementations without touching UserService. I can test UserService with a mock. I can even have different modules use different databases.
Visualizing the Change
BEFORE: Unstable depending on unstable
┌──────────┐ ┌──────────┐│UserService│────▶│PostgreSQL ││(changes │ │(changes ││frequently)│ │frequently)│└──────────┘ └──────────┘
Result: Fragile, hard to test, hard to changeAFTER: Unstable depending on stable
┌──────────┐ ┌──────────┐ ┌──────────┐│UserService│────▶│Database │◀────│PostgreSQL ││(unstable) │ │Interface │ │(unstable) ││ │ │(STABLE) │ │ │└──────────┘ └──────────┘ └──────────┘
Result: Testable, flexible, maintainableThe key insight: stable doesn’t mean “never changes.” It means “changes less frequently.” Interfaces change less than implementations. Abstract concepts change less than concrete details.
Putting Both Rules Together
Here’s a pattern I now use in almost every project:
from abc import ABC, abstractmethodfrom queue import Queuefrom threading import Thread
# STABLE INTERFACE (rarely changes)class DataStore(ABC): @abstractmethod def save(self, data): pass
# UNSTABLE IMPLEMENTATION (can change anytime)class FileDataStore(DataStore): def save(self, data): # File-specific implementation pass
class DatabaseDataStore(DataStore): def save(self, data): # Database-specific implementation pass
# SINGLE WRITER (only one place that calls save())class DataWriter: def __init__(self, store: DataStore): # Depends on stable interface self._store = store self._queue = Queue()
def write(self, data): # All data mutations queued here self._queue.put(data)
def process(self): # Single point where data actually gets saved while True: data = self._queue.get() self._store.save(data)
# UNSTABLE CLIENTS (don't mutate data directly)class AnalyticsModule: def __init__(self, writer: DataWriter): # Depends on stable writer self._writer = writer
def track_event(self, event): self._writer.write(event) # Doesn't touch data directly
class UserModule: def __init__(self, writer: DataWriter): # Depends on stable writer self._writer = writer
def update_profile(self, user_data): self._writer.write(user_data) # Doesn't touch data directlyBenefits I get from this architecture:
- No race conditions:
DataWriterserializes all mutations - Easy testing: Mock
DataStoreto testDataWriter, mockDataWriterto test modules - Independent modules:
AnalyticsModuledoesn’t know aboutUserModule - Change resilient: Swap
FileDataStoreforDatabaseDataStorewithout touching clients
Common Mistakes I Made
Mistake 1: Treating All Data as Shared
I used to think “if multiple things need this data, it should be shared everywhere.” That led to chaos.
Fix: Identify data ownership early. Who “owns” this data? Make them the single writer.
Mistake 2: Stable Modules Depending on Unstable Ones
I once built a configuration module (very stable, rarely changes) that depended on a specific logging library (changed every month). Every time the logging library updated, my config broke.
Fix: Use dependency inversion. Make stable modules depend on stable abstractions, not unstable implementations.
Mistake 3: Ignoring SOLID as “Too Academic”
I dismissed SOLID principles as theoretical nonsense for years. Then I realized the two rules I learned are actually distilled versions of:
- Single Responsibility Principle: Each module does one thing (single writer embodies this)
- Dependency Inversion Principle: Depend on abstractions, not concretions (stable dependencies embodies this)
Mistake 4: Not Learning from Established Patterns
I kept trying to reinvent solutions. Then someone pointed me to Martin Fowler’s work. As one architect put it: “You’ll find that Martin Fowler already named them for you.”
Resources that helped me:
- Clean Architecture by Robert C. Martin - explains WHY these principles matter
- Patterns of Enterprise Application Architecture by Martin Fowler - classic design patterns
- SOLID principles - the theoretical foundation behind practical rules
How I Use These Rules Now
Every time I design a new feature, I ask two questions:
- For data: “Who owns this data? Can I make them the single writer?”
- For modules: “Which direction do dependencies point? Are unstable things depending on stable things?”
These questions have become second nature. They help me reason about problems instead of just guessing.
When I encounter a race condition, I check for multiple writers.
When I can’t test something in isolation, I check for unstable dependencies on unstable things.
When a small change breaks everything, I check for tight coupling that violates the stability principle.
The Bottom Line
You don’t need to memorize dozens of architecture patterns. Two simple rules will carry you far:
- Single writer for data: Eliminates race conditions and makes debugging predictable
- Stable dependencies for modules: Enables testing and reduces change propagation
Start applying these in your current codebase. Ask “who owns this data?” and “which way do my dependencies point?” The answers will reveal most architecture problems before they become disasters.
For deeper learning, I recommend starting with Uncle Bob’s Clean Architecture and Martin Fowler’s design patterns. They provide the theoretical foundation that makes these practical rules even more powerful.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Reddit Discussion on Architecture Principles
- 👨💻 Clean Architecture by Robert C. Martin
- 👨💻 Patterns of Enterprise Application Architecture
- 👨💻 SOLID Principles
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments