software engineering best practices

Software Engineering Best Practices: 10 That Matter

By Zest | Updated | 8 min read
Several monitors showing code, the middle one with the words Engineering Excellence

The ten software engineering best practices that matter most are: a clear Git workflow, test-driven development, code review, CI/CD, agile iteration, clean code and refactoring, an automated testing strategy, documentation, security by design and modular architecture. Each one below has a short definition and the concrete steps to start.

Zest shows which coding agents, models and skills each engineer on your team uses, and what ships from them. Start free

None of them is new. What’s new is that coding agents now write a growing share of the code, which makes the checks around the code (tests, review, CI) more important, not less. A short section at the end covers that.

1. Version Control and Git Workflow

Version control records every change to the codebase, so a team can work in parallel, review changes before they land and go back to any earlier state. Git is the standard, and it scales: Microsoft moved Windows development to Git in 2017, a repository of about 3.5 million files.

How to implement a Git workflow

A feature-branch workflow suits most teams. Trunk-based development (short-lived branches merged at least daily) suits teams with strong CI; GitFlow’s long-lived branches mostly suit software shipped in versioned releases.

  • One branch per change: feature/user-authentication, fix/login-timeout.
  • Small, clear commits: for example feat: add password reset endpoint, following Conventional Commits if you want changelogs generated from history.
  • Open a pull request into main when the change is ready.
  • Merge only after checks and review: CI passes, a teammate approves, and branch protection on main enforces both.

2. Test-Driven Development (TDD)

Test-driven development means writing a failing test before the code that makes it pass. You work in a short red-green-refactor loop, so every piece of code exists to satisfy a test and the design is testable from the start.

How to implement TDD

  • Red: write one small test for the next piece of behavior and watch it fail.
  • Green: write the simplest code that makes it pass.
  • Refactor: clean up the code while all tests stay green.

In the best-known industrial study, four teams at Microsoft and IBM that adopted TDD cut pre-release defect density by 40–90%, at the cost of 15–35% more initial development time (Nagappan et al., 2008). Our red-green-refactor guide walks through a full example.

3. Code Review and Pair Programming

Code review means a teammate reads a change before it merges; pair programming means two developers write it together in real time. Both catch defects early and spread knowledge of the codebase, so no part of it depends on one person.

How to implement code reviews and pairing

  • Keep pull requests small. Google’s review guide calls about 100 lines a reasonable change and 1,000 lines usually too large (Google engineering practices).
  • Automate style. Formatters and linters (Prettier, Biome, Black, Ruff) settle formatting, so reviewers look at logic, design and risk.
  • Review the code, not the author. Phrase comments as questions or suggestions and explain why.
  • Review within a day. A pull request that waits loses its author’s context.
  • Pair on hard problems, onboarding and risky fixes, and switch the “driver” (typing) and “navigator” (thinking ahead) roles often.

More in our guide to code review good practices.

4. Continuous Integration and Continuous Deployment (CI/CD)

Continuous integration builds and tests every change automatically as soon as it’s pushed; continuous deployment then releases every change that passes to production. Together they make releases small, frequent and routine instead of rare and risky.

How to implement a CI/CD pipeline

  • Start with CI: run the build and the tests on every push and pull request (GitHub Actions, GitLab CI/CD, Jenkins and others; see our CI/CD platforms comparison).
  • Keep it fast: Extreme Programming’s “ten-minute build” is still a good target. A slow pipeline is one developers learn to ignore.
  • Automate deployment: once CI is reliable, deploy every green build to staging, then to production.
  • Use feature flags to separate deploying code from releasing a feature, so unfinished work can ship switched off.

5. Agile and Iterative Development

Agile development delivers software in small increments and adjusts the plan based on feedback, instead of following a fixed plan through sequential phases. Teams ship working software often and learn from real use.

How to implement an agile framework

Scrum is the most common framework. Its 2020 Scrum Guide defines:

  • Three accountabilities: a Product Owner (what to build and in what order), a Scrum Master (how the team works) and the Developers (who build it).
  • Sprints of one month or less, most often two weeks, each starting with sprint planning.
  • A Daily Scrum of at most 15 minutes to check progress toward the sprint goal and surface blockers.
  • A Sprint Review with stakeholders and a Sprint Retrospective for the team, at the end of every sprint.

Kanban, with continuous flow and work-in-progress limits instead of sprints, is the common alternative for support and platform teams.

6. Clean Code and Refactoring

Clean code is code that other people can read, understand and change safely. Refactoring is the habit of improving the structure of existing code without changing what it does, so technical debt is paid down a little at a time.

How to implement clean code and refactoring

  • Leave code a little cleaner than you found it. Rename a confusing variable or split a long function while fixing a bug nearby.
  • Use names that say what things do: fetchUserProfile, not getUsrDat.
  • Keep functions small, with one job each; return early instead of nesting if/else blocks.
  • Refactor under tests. Without tests, a refactor is a guess.
  • Let tools enforce style with a linter and formatter run in CI, so style never reaches review.

7. Automated Testing Strategy

An automated testing strategy decides which kinds of tests to write and how many of each, so the suite catches regressions quickly without becoming slow and fragile. Tests written as code run on every change, so nobody has to re-test by hand.

How to implement an automated testing strategy

The test pyramid, popularized by Mike Cohn, is a good default:

  • Many unit tests for functions and classes in isolation. They run in milliseconds and point straight at the failure.
  • Fewer integration tests for how parts work together, such as an API endpoint reading from a real database.
  • A few end-to-end tests for critical user journeys through the UI (Playwright, Cypress). They are slow and break more easily, so keep them to what matters most.
  • Run them all in CI on every pull request, and fix or quarantine flaky tests quickly, because one flaky test teaches the team to ignore red builds.

8. Documentation and Knowledge Management

Documentation records what code can’t say for itself: why the system is built this way, how to run it, and how its parts and APIs fit together. It lets new people get productive without interrupting the ones who already know.

How to implement documentation and knowledge management

  • Write down the why. Code shows what it does; comments, READMEs and commit messages should say why this approach and not another.
  • Keep docs in the repository, as Markdown next to the code (README.md, CONTRIBUTING.md), so they change in the same pull request as the code.
  • Record big decisions as Architecture Decision Records (ADRs): a short note of the context, the decision and its consequences.
  • Generate reference docs from the code where you can: OpenAPI for HTTP APIs, JSDoc or docstrings for libraries.
  • Write instructions for coding agents too. A short CLAUDE.md or AGENTS.md with the build and test commands and the project’s conventions saves every agent session from guessing.

More in our code documentation best practices.

9. Security by Design and DevSecOps

Security by design means building security into every stage of development rather than checking for it at the end. DevSecOps puts that into practice with security checks in the CI/CD pipeline, so every change is scanned and the whole team shares responsibility.

How to implement DevSecOps

  • Scan code in CI: static analysis (SAST) on every pull request, dynamic testing (DAST) against a running staging app.
  • Watch dependencies: Dependabot, Renovate or Snyk open pull requests when a library you use has a known vulnerability.
  • Threat-model new features: before building, list how the feature could be abused (the OWASP Top 10 is a useful checklist) and plan the defenses.
  • Keep secrets out of code: store keys in a secret manager (HashiCorp Vault, AWS Secrets Manager) and turn on secret scanning so a leaked key is caught at push time.

Microsoft’s Security Development Lifecycle, in use since 2004, is a well-documented model to borrow from.

10. Modular Architecture and Design Patterns

Modular architecture splits a system into parts with clear boundaries that can be built, tested and changed independently. Design patterns are proven solutions to problems that recur inside and between those parts.

How to implement modular design

Modular doesn’t mean microservices. A well-structured monolith with clear internal modules is often the better start; split out services when a part needs to scale or deploy on its own.

  • Draw boundaries around business capabilities, using domain-driven design’s bounded contexts.
  • Define contracts between modules, such as an OpenAPI spec or typed interfaces, so each side can change without breaking the other.
  • Use patterns where they fit: layered architecture, adapters for external services, circuit breakers for unreliable dependencies. Don’t add a pattern before you have the problem.
  • Record the decisions in ADRs.

Where to start

Don’t adopt all ten at once. Pick the one that hurts most:

  • Bugs keep reaching production: add tests to CI and practice TDD on the module that breaks most often.
  • Deployments are slow and manual: set up CI on every push, then automate deploys to staging.
  • Nobody knows why the code looks this way: start ADRs and require doc updates in pull requests.
  • Style arguments in every review: add a formatter and linter as a required CI check.

What changes when coding agents write the code

Every practice above still applies, and some matter more:

  • Tests become the spec the agent checks itself against. Ask for a failing test first, then the fix.
  • Small pull requests still win. An agent can produce 2,000 lines in minutes; reviewers still read 100 at a time.
  • CI is the gate. Agent-written code goes through the same pipeline and the same review as anyone’s.
  • Project instructions belong in the repo. Keep CLAUDE.md or AGENTS.md and reusable skills under version control, so every engineer’s agent starts from the same conventions.

In one small team’s 866 agent-written pull requests, coding was 43% of the agents’ session time, and planning and local testing took half; Zest shows which agents, models and skills each engineer uses and what ships.