DEV Community

Cover image for 💡 How to Write Better Code
Ivan Zaitsev
Ivan Zaitsev

Posted on

💡 How to Write Better Code

Beginning

Let's be honest, most developers think their code looks good. The truth is, everyone can write code that works. Only a few write code that lasts.

And what’s the difference? It's not about intelligence, it's about discipline, consistency, and taste.

Did you read Clean Code book? Forget about it.

Below is a guide not just to writing "working" code, but to writing beautiful, reliable, and maintainable code, the kind that earns silent nods from future engineers who read it.

Introduction

Do you think your code looks good?

If your colleagues tell you it does, don't be so sure. Chances are, they've just forgotten what truly good code looks like.

The real question is: can you actually tell the difference between good and bad code?

Everyone has their own idea of what "good" means, and that's part of the problem.

In reality, software engineers progress through different stages of maturity when it comes to code quality:

  1. No sense of cleanliness – Ignores feedback, invents a personal and inconsistent coding style.

  2. Follows rules blindly – Applies every rule from a coding book rigidly, regardless of context.

  3. Understands context – Appreciates good practices and applies them appropriately when they fit.

  4. Masters adaptability – Writes and refactors code so that good principles become applicable in more situations.

Scope Matters

When a project has only a hundred lines of code, you can navigate it easily, even if it's messy.

But real production systems are far larger. When inconsistency and disorder scale up, maintaining the system becomes a nightmare.

Good naming and clarity might feel unnecessary in a tiny snippet.

But now imagine a codebase with hundreds of thousands of lines.

If developers use verbs interchangeably - "build", "create", "generate", "make", the code starts lying to you.

You read a function name and have no clue what it truly does. Confusion becomes the default.

And in the high-pressure reality of software development, shortcuts sneak in:

  • Proof of Concepts
  • Workarounds
  • Temporary fixes

But here's the truth: everything labeled "temporary" eventually becomes permanent.

So yes - the struggle is real. I understand your pain.

And if your friends tell you, "What's hard to write should be hard to read" you should ask yourself: are they really your friends?

Steps to Writing Good Code

Writing good code doesn't happen by accident - it's a process.

It takes discipline, attention to detail, and the willingness to question your own habits.

Below are a few key steps that can help you build clean, maintainable, and scalable code.

1 Preparation

Before you write a single line of code, make sure your environment, both mental and technical is ready.

  • Focus

Clear your head and minimize distractions. Quality code comes from deliberate thought, not rushed typing.

  • Understand the problem

Don't start coding until you know what you're solving.

A clear understanding of requirements prevents wasted effort and endless refactoring later.

  • Plan your structure

Think about how your modules, classes, and methods will interact.

Even a rough mental outline can help you avoid messy dependencies and poor abstractions.

2 Naming

Naming is one of the hardest and most important parts of programming.

Good names make your code self-explanatory, bad names make it painful to read and maintain.

  • Use unified naming style

Consistency is key. Follow a single convention across your entire codebase for variables, classes, and methods.

  • Don't use magic numbers and values

Replace hard-coded values with named constants or configuration parameters.

It makes your code clearer and easier to modify later.

  • Don't use redundant prefixes or suffixes

Avoid using redundant suffixes or prefixes in the name of variables, fields, methods, classes, and other places. Such as Map, List, and others.

  • Use different names for different actions

Avoid reusing the same verb for unrelated operations.

For instance, naming one method create() to build an object and another create() to save it to the database adds confusion. Choose distinct names that reflect the real intent, like buildUser() and saveUser().

3 Logic

  • Don't reinvent the wheel

Use standard libraries, proven frameworks, and community best practices.

Reinventing existing solutions wastes time and introduces unnecessary bugs.

  • Avoid overengineering

Simplicity is strength. Don't add abstractions or features you might need later, they only make the system harder to maintain.

  • Don't reassign variables

Reassigning values often indicates that your logic could be split into smaller, clearer methods.

  • Don't modify method input arguments

Changing arguments inside a method makes the code unpredictable and confusing. Treat inputs as immutable.

  • Avoid deep nesting

Nested blocks reduce readability. Use patterns like "early return" and "method extraction" to simplify complex logic.

  • Don't write unnecessary comments

Good code explains itself.

When names and structure are clear, you won't need to comment on what each line does, the code will speak for itself.

4 Development Principles

Writing good code also means following time tested development principles.

These principles act as guardrails that help you design maintainable, flexible, and scalable systems.

  • Follow established industry principles

Principles such as SOLID, KISS, DRY, YAGNI and others form the foundation of clean software design.

For example:

  • Break a large method with many responsibilities into several smaller, focused ones.

  • Reuse existing functionality instead of writing new code from scratch.

  • Replace direct dependencies with abstractions or interfaces.

  • Replace static utility classes with regular ones that can be injected and tested.

  • Apply design patterns when appropriate

Design patterns offer proven solutions to recurring problems.

For instance:

  • Apply the Strategy or Command pattern instead of writing a chain of IF or SWITCH statements when different behaviors depend on input parameters.

  • Follow best practices for your framework

Each framework has its own idiomatic style and set of recommended patterns.

Learn them, follow them, and rely on official documentation whenever possible.

5 Consistency

Consistency is the backbone of maintainable code.

Even average code can be manageable if it's consistent, but excellent code becomes painful to work with when it's not.

A consistent codebase is easier to read, understand, and extend.

Once a specific approach or convention is chosen, stick to it throughout the project.

Consistency applies across all layers of the system.

  • Code style

Enforcing code style manually is nearly impossible in large projects.

Use automated tools like Spotless, Checkstyle to maintain consistent formatting and structure.

  • Text style (errors, logs, TODOs)

Standardize your message formats.

Logging, error messages, and TODO comments should follow a unified convention across the entire codebase.

  • Naming of variables and fields

The same entities across the project should always be named consistently.

If something is called customerId in one module, it shouldn't be custId elsewhere.

  • Method and class formatting

Keep a consistent layout for how classes, methods, and imports are structured.

  • Package and module structure

Maintain the same structural organization throughout the system.

A unified project structure helps new developers understand the architecture faster.

6 Isolation

Good systems isolate concerns.

Each part of your application should have a single, clear responsibility, and changes in one area should not break or require changes in another.

When business logic is tightly coupled with infrastructure or frameworks, flexibility and testability suffer.

True isolation ensures that your core logic remains clean, reusable, and easy to adapt as technology changes.

  • Business logic code should be isolated from external dependencies

External dependencies include anything that lies outside your domain logic, such as:

  • File system
  • HTTP calls
  • Databases
  • Message queues
  • External libraries
  • Frameworks
  • Cloud services

All such dependencies should be abstracted into separate layers or modules.

This keeps your business logic clean, testable, and framework independent.

For example:

  • If your configuration code depends directly on a cloud service like Azure Key Vault, you're introducing a vendor lock-in and reducing flexibility.

Instead, isolate that dependency or even better, move it to the deployment layer.

  • Avoid environment dependencies

Business logic should behave the same way regardless of the environment.

When behavior must differ between environments, use Feature Flags rather than hard-coding conditional logic.

7 Structure

The structure of your codebase defines how easily it can evolve over time.

A clear, consistent structure helps developers navigate the system and avoid breaking established boundaries.

  • Follow single code structuring approach

Choose one architectural organization style and apply it consistently.

For example:

  • Packaging by Layer – group classes by technical layer (e.g. controller, service, repository).

  • Packaging by Feature – group all classes related to a specific business feature together.

  • Avoid module/layer boundary violation

Respect module and layer boundaries.

For example:

  • For "Packaging by Layer" the highest layer should never call the lowest directly, bypassing business logic.

  • For "Packaging by Feature", one module should never reach into another module's internal code.

8 Performance

Performance should never be an afterthought.

Even the cleanest code can cause serious issues if it doesn't scale or handle load efficiently.

Many performance problems arise from the misuse of frameworks or a lack of understanding of how they operate internally.

  • Avoid performance pitfalls when using frameworks

Frameworks often simplify development but can hide performance bottlenecks if used incorrectly.

Common examples include:

  • Excessive JOIN operations in SQL queries, which may accidentally produce Cartesian products and drastically slow down queries.

  • Overreliance on ORMs that lead to issues like the N+1 query problem, where multiple redundant database calls are made.

  • Misuse of transactions that span an entire method, especially when blocking operations are involved, this can cause unnecessary locks and degrade performance.

Understanding the behavior of your framework and database engine helps you write code that is both elegant and efficient.

9 Testing

Testing is the safety net of software quality.

Well-written tests prevent regressions, support refactoring, and ensure that your code behaves as expected.

A good rule of thumb: unit tests protect your logic, integration tests protect your system behavior.

  • Cover business logic with unit tests

Unit tests verify the correctness of individual classes or functions in complete isolation.

Replace all dependencies with mocks or stubs.

Test only public methods, private methods should be validated indirectly through those public interfaces.

Include both positive and negative test cases, covering every conditional path within the method.

Unit tests help ensure your logic remains consistent even as the surrounding code evolves.

  • Cover integrations with external systems using integration tests

Integration tests validate how your system interacts with real or simulated external components, databases, APIs, queues, etc.

These tests should be as close as possible to the production environment while remaining stable and reproducible.

For example, you can use tools like MockServer or TestContainers to emulate dependencies without introducing heavy setup or external configurations.

10 Refactoring

Refactoring is an ongoing process, not a one-time event.

Attempting a complete rewrite of a large codebase is usually impractical, risky, and time-consuming.

Instead, aim for incremental improvement. Refactor gradually and iteratively.

Focus on improving the parts of the code you're already working on as part of your regular tasks.

Over time, these small refinements compound into a major quality improvement.

Clean up naming, simplify logic, remove duplication, and apply good principles as you touch the code.

Continuous, small-scale refactoring is safer, more manageable, and far more effective than trying to fix everything at once.

Final Thoughts

Writing good code is a journey.

Good code isn't about perfection, it's about progress.

It's about caring enough to make things a bit clearer, a bit cleaner, a bit easier for the next person who reads your code (even if that person is future you).

Anyone can write code that works. But writing code that feels good to read, that's craftsmanship.

And remember - if you write bad code, the next person might come looking for you.

Top comments (0)