Software Engineering
Can You Really Learn OOP Without Breaking It First?
My OOP Learning Curve
Introduction
Today, I was listening to Krzysztof Kempiński’s podcast with Sobotka and Pilimon, featuring some DDD experts. It reminded me of my past and recent mistakes in designing (or not designing) software architecture.
I believe there’s a universal truth—whether in art or programming—that to break the rules, you first need to understand them. If you don’t know the principles or their consequences, you might be creating technical debt without even realizing it.
The problem with programming is that rules aren’t always obvious. You often have to dig deep into the code to fully understand their impact. Unless, of course, you have Linus Torvalds’ IQ. 😉
It wasn’t hard to recall my design mistakes. Probably because I felt them deeply—these are the moments that stick with you. Just reading theory doesn’t hit the same way.
Disclaimer: Some of these mistakes are from more than six years ago.
I had read about many of them in books, podcasts, and articles, but true understanding only came with humbling experiences.
Overengineering and Following Rules Too Strictly
☑ Creating unnecessary layers because that’s what the course said – blindly following layered architecture without questioning its value.
☑ ServiceManager as a "God class" – putting all logic into one giant service instead of spreading it across responsible objects.
☑ Overengineering – adding patterns that made things more complicated rather than solving problems.
Non-OOP Code in an OOP Language
☑ Anemic domain model – classes full of getters and setters instead of a rich model based on behavior.
☑ Starting with nouns instead of behaviors – I began by defining entities and their relations instead of focusing on the domain and its actions.
☑ 1:1 mapping between views and repositories – database queries were almost directly exposed in the UI layer.
☑ Tightly coupled components – making the code difficult to refactor and reuse.
☑ No control over access modifiers – everything was public, even when it wasn’t necessary.
☑ Relying on concrete implementations instead of abstractions – making the code hard to test and impossible to swap components.
☑ Ignoring SOLID, GRASP, and other OOP principles – leading to most of the issues above.
Testing and Code Design
☑ No tests or testing without TDD – which led to classes that were difficult to test. Writing the test first often forces better design.
☑ UI-first instead of backend-first – business logic leaking into the frontend. Now I know I prefer validation and exceptions on the backend over “great UX” at the cost of logic.
Misunderstanding Architecture
☑ Wrong interpretation of Clean Architecture – like putting repositories in the domain instead of the infrastructure layer.
☑ Logic based on data structures – using hashmap-in-hashmap solutions instead of real objects.
Blind Faith in Tools and Technologies
☑ Over-reliance on frameworks – not isolating from the tool and abusing its built-in functionality.
☑ Hibernate and N+1 / lazy loading issues – using ORM incorrectly, leading to performance problems and debugging nightmares.
☑ CRUDs? Fine for hobby projects, but in large systems, are stored procedures a better choice...? 🤔
☑ Not understanding low-level mechanisms – which led to design issues, like initializing controllers incorrectly in JavaFX with MVVM.
Reflection on Reflection
☑ Reflection – powerful, but is it always worth it? – I used it without understanding its performance cost and debugging difficulties.
☑ Spring as a black box – at first, it seemed like magic. Later, I realized it was just reflection and proxies.
Conclusion
There’s no doubt that these mistakes taught me more than any course or training.
Thanks to them, I now better understand frameworks like Spring. When I came back to it after a while (when it still seemed like a black box because of reflection magic), many concepts suddenly made sense.
Software engineering isn’t about rigid rules, but heuristics. Every problem should be analyzed individually, considering available resources and real needs. Sometimes trade-offs are necessary, even if nobody likes them.