DI, SOLID, and the Fundamentals That Actually Matter
I stared at the screen. Lesson 58: Dependency Injection. Five minutes. Checkbox checked. Done.
But I wasn't done. Not really.
These four lessons changed how I write code. DI. SOLID. Naming Conventions. Exception Handling. They're not just concepts. They're survival skills. Let me explain what they actually mean.
Dependency Injection
Dependency injection is about control. Instead of a class creating its own dependencies, you pass them in. That's it.
Without DI, a service creates its own database connections and email clients. You can't test it without hitting real systems. You can't swap implementations. Everything is hardcoded.
With DI, you pass dependencies in. The service doesn't care about the implementation. It cares about the contract. Testing becomes easy. Swapping implementations becomes trivial. Changing one thing doesn't break another. This connects directly to the architecture principles I follow for building systems that actually work.
SOLID Principles
SOLID is an acronym. Five principles that make code maintainable. I've written about coding principles before, but these five deserve their own deep dive.
Single Responsibility Principle: A class should have one reason to change. One job.
A PaymentProcessor that processes payments, sends emails, logs to files, and updates analytics violates this. Split it. PaymentProcessor processes payments. EmailService sends emails. Logger handles logging. Each class has one job.
Open-Closed Principle: Open for extension, closed for modification. Add new features without changing existing code.
Instead of adding if statements for each payment method, use a plugin pattern. Each payment method implements a PaymentMethod interface. To add a new method, create a new class. No changes to existing code.
Liskov Substitution Principle: Subtypes must be substitutable for their base types.
A Square that extends Rectangle but breaks the contract when you call setWidth() violates this. If code expects a Rectangle, passing a Square shouldn't break it. Use composition when inheritance doesn't make sense.
Interface Segregation Principle: Clients shouldn't depend on interfaces they don't use. Keep interfaces small.
A UserService interface with 20 methods forces every client to depend on everything. Split it. AuthenticationService for auth. ProfileService for profiles. NotificationService for notifications. Each service depends only on what it needs.
Dependency Inversion Principle: Depend on abstractions, not concretions.
Instead of directly creating concrete classes, depend on interfaces. High-level modules don't depend on low-level modules. Both depend on abstractions. This is where dependency injection shines.
Naming Conventions
Good naming isn't about being clever. It's about being clear.
I've seen generic names for everything. Data for things that weren't data. Temp for things that weren't temporary. Functions named doStuff that did specific things. Code that required a decoder ring to understand.
Name things explicitly. Process payment, not handle. Payment service, not service. User email address, not email when there are multiple email fields.
It takes longer to write. But it takes much less time to read. And reading code happens way more than writing it. Good naming is one of those essential coding principles that seems obvious until you see the mess bad naming creates.
Exception Handling
Exceptions are for exceptional cases. Things that shouldn't happen. Things that indicate bugs or system failures.
Not for normal business logic. Not for validation errors. Not for things you expect.
I've seen code that throws exceptions for everything. User not found? Exception. Invalid input? Exception. Database timeout? Exception. The calling code becomes a try-catch nightmare.
I've also seen code that never throws exceptions. Everything returns error codes. Null checks everywhere. The code becomes defensive and hard to follow.
The truth? Business logic errors should be return values. Only system failures should be exceptions. The code becomes cleaner. The error handling becomes predictable. This is part of writing code that's actually testable and maintainable.
Why These Matter
These lessons aren't just concepts. They're the difference between code that works and code that works well. Between systems that scale and systems that break. Between teams that move fast and teams that move carefully. They're part of building systems that actually scale and follow solid engineering processes.
I learned them through mistakes. Through production incidents. Through debugging sessions that lasted hours. Through code reviews where I had to explain why I did things the wrong way.
The checkbox next to DI is checked. But the learning never stops. Every codebase teaches me something new. Every mistake makes me better. Every refactoring shows me a better way.
Which lesson changed everything for you? Was it DI? SOLID? Something else entirely?
