Appearance
Phase 2: Develop
The Development phase is where plans become working software. It encompasses system design, environment setup, coding, collaboration, and the daily practices that determine whether a codebase remains healthy or accumulates crippling technical debt. A disciplined development phase turns the Research phase's deliverables into a reliable, maintainable application.
System Architecture and Design
Before the first feature branch is created, the team must translate the architecture decisions from Research into a concrete system design.
Architectural Patterns
The choice of architectural pattern shapes nearly every aspect of development. A monolithic architecture packages all functionality into a single deployable unit. It is simpler to develop, test, and deploy initially, making it well-suited for small teams and early-stage products. However, as the codebase grows, monoliths can become difficult to scale, modify, and reason about.
A microservices architecture decomposes the system into small, independently deployable services, each owning a specific business capability. Microservices enable independent scaling and deployment, technology heterogeneity (different services can use different languages or databases), and team autonomy. The trade-offs include increased operational complexity, network latency between services, and the need for robust service discovery, API gateways, and distributed tracing.
An event-driven architecture uses events (messages representing state changes) as the primary communication mechanism between components. Producers emit events to a broker (such as Kafka, RabbitMQ, or Amazon SNS/SQS), and consumers react to them asynchronously. This pattern excels at decoupling services, handling high-throughput workloads, and supporting real-time processing pipelines.
Many real-world systems use a hybrid approach, combining a modular monolith with event-driven integrations for specific high-throughput concerns, or starting monolithic and extracting microservices as the product matures.
API Design
APIs are the contracts between components, services, and external consumers. Thoughtful API design prevents breaking changes and reduces integration friction. REST remains the most widely used paradigm for web APIs, emphasizing resource-oriented URLs, standard HTTP methods, and statelessness. OpenAPI (Swagger) specifications provide a machine-readable contract that can generate documentation, client SDKs, and validation logic automatically.
GraphQL offers a flexible query language that lets clients request exactly the data they need, reducing over-fetching and under-fetching. It is particularly valuable for applications with diverse client needs (mobile, web, third-party). gRPC uses Protocol Buffers for high-performance, strongly typed communication and is commonly chosen for internal service-to-service calls where latency matters.
Regardless of the paradigm, good API design includes consistent naming conventions, clear error response formats with actionable messages, versioning strategy (URL path versioning, header versioning, or content negotiation), pagination for list endpoints, and rate limiting and authentication from the start.
Database Design
Database schema design has long-lasting consequences because data outlives code. Relational databases require careful normalization to eliminate redundancy, with strategic denormalization where read performance demands it. Entity-relationship diagrams (ERDs) visualize tables, columns, relationships, and constraints before any SQL is written.
For document-oriented databases, schema design focuses on access patterns: documents should be structured to minimize the number of queries required for common operations. Time-series, graph, and key-value databases each have their own modeling best practices dictated by their query models.
Data modeling should account for growth, including expected data volumes, query frequency, indexing strategy, and archival or partitioning plans. Migration tooling (Flyway, Alembic, Liquibase) should be configured from day one so that schema changes are versioned, repeatable, and reversible.
Development Environment Setup
A well-configured development environment eliminates friction and ensures consistency across the team.
Version Control
Git is the industry standard for version control. The repository should be initialized with a clear directory structure, a comprehensive .gitignore file, and branch protection rules enforced on the main branch. Commit messages should follow a consistent convention — Conventional Commits (e.g., feat:, fix:, docs:, refactor:) is a widely adopted standard that enables automated changelog generation and semantic versioning.
Branching Strategy
The branching strategy should match the team's size and release cadence. Trunk-based development keeps branches short-lived (hours to a day) and merges frequently into main, relying on feature flags to hide incomplete work. This approach minimizes merge conflicts and encourages continuous integration. GitHub Flow uses feature branches that merge into main via pull requests, with main always deployable. It strikes a balance between collaboration and simplicity. GitFlow introduces dedicated branches for development, releases, and hotfixes. It provides more ceremony and control, which can be valuable for teams with formal release cycles or compliance requirements.
Local Environment
Every developer should be able to set up a working local environment with minimal effort. Tools and practices that enable this include containerized dependencies using Docker and Docker Compose so that databases, message brokers, and other services run identically on every machine, environment variable management through .env files (excluded from version control) with a .env.example template checked in, dependency lockfiles (package-lock.json, poetry.lock, Cargo.lock) to ensure deterministic installs, and a Makefile, Taskfile, or shell scripts that automate common operations like seeding the database, running migrations, and starting the development server.
Code Quality Tooling
Automated tooling enforces consistency and catches issues before code review. Linters and formatters (ESLint, Prettier, Ruff, Black, Clippy) should be configured with a shared ruleset and ideally enforced via pre-commit hooks using tools like Husky or pre-commit. Static analysis tools (SonarQube, Semgrep, CodeQL) detect code smells, security vulnerabilities, and complexity issues. Editor configuration files (.editorconfig, workspace settings) ensure consistent indentation, line endings, and file encoding across IDEs.
Writing Code
The daily work of development centers on writing, reviewing, and integrating code.
Coding Principles
Principles guide decision-making when there is no explicit rule. SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) promote modular, extensible, and testable object-oriented designs. DRY (Don't Repeat Yourself) encourages extracting shared logic rather than duplicating it, but should be balanced against premature abstraction. YAGNI (You Aren't Gonna Need It) guards against building features or abstractions that are not yet needed. KISS (Keep It Simple, Stupid) favors straightforward solutions over clever ones. The principle of least surprise says that code should behave the way a reader would expect.
These principles sometimes conflict — DRY can push toward abstractions that violate KISS, for example. Good engineering judgment means knowing when to favor one principle over another based on the specific context.
Writing Clean Code
Clean code is code that is easy to read, understand, and modify. It uses intention-revealing names for variables, functions, classes, and modules. Functions are small and do one thing. Side effects are explicit and minimized. Error handling is thoughtful — functions either succeed or fail clearly, with meaningful error messages. Comments explain "why," not "what" (the code itself should make "what" obvious). Magic numbers and strings are replaced with named constants. Nested conditionals are flattened through early returns or guard clauses.
Test-Driven Development
Test-driven development (TDD) inverts the typical workflow: the developer writes a failing test first, then writes the minimum code to make it pass, then refactors. This cycle (Red, Green, Refactor) produces code that is testable by design, provides a living specification of expected behavior, catches regressions immediately, and encourages small, focused functions. TDD is not appropriate for every situation — exploratory prototyping, UI layout, and highly uncertain designs may benefit from a test-after approach — but for business logic and data transformations, it consistently produces higher-quality code.
Dependency Management
External dependencies accelerate development but introduce risk. Best practices include pinning dependency versions to avoid unexpected breakage from upstream changes, regularly auditing dependencies for known vulnerabilities using tools like npm audit, pip-audit, or Dependabot, evaluating dependencies before adoption by checking maintenance activity, license compatibility, download counts, and issue backlog, and minimizing dependency surface area by avoiding libraries for trivial tasks that can be implemented in a few lines.
Collaboration and Code Review
Software development is a team activity, and the quality of collaboration directly impacts the quality of the product.
Pull Requests and Code Reviews
Pull requests (PRs) are the primary mechanism for integrating work and sharing knowledge. An effective PR includes a clear title and description explaining what changed and why, a link to the relevant ticket or issue, a focused scope — ideally one logical change per PR, screenshots or recordings for UI changes, and evidence that tests pass and no regressions were introduced.
Code review is not just about catching bugs — it is about shared ownership, knowledge transfer, and maintaining standards. Reviewers should focus on correctness (does the code do what it claims?), design (is the approach appropriate? are there simpler alternatives?), readability (can a new team member understand this?), testing (are edge cases covered? are tests meaningful?), and security (are inputs validated? are secrets handled safely?).
Reviews should be constructive and specific. "This is wrong" is unhelpful; "This SQL query is vulnerable to injection because user input is interpolated directly — consider using parameterized queries" is actionable. Teams should establish norms around review turnaround time (ideally within a few hours) to avoid blocking teammates.
Pair and Mob Programming
Pair programming places two developers at one workstation, with one typing (the driver) and one reviewing and thinking strategically (the navigator). Mob programming extends this to the entire team working on a single task together. Both practices accelerate knowledge sharing, catch errors in real time, and are particularly valuable for complex or unfamiliar problem domains. They are best used selectively rather than as a default for all work.
Documentation
Code-level documentation (docstrings, type annotations, inline comments for non-obvious logic) helps future developers — including the original author — understand intent. Project-level documentation should include a README covering project purpose, setup instructions, and architecture overview, an architecture diagram showing major components and their interactions, a contributing guide describing the workflow for making changes, and an API reference generated from code annotations or specification files. Documentation should be treated as a living artifact, updated alongside the code it describes.
Managing Technical Debt
Technical debt is the gap between the current state of the codebase and the ideal state. Some debt is intentional (taking a shortcut to meet a deadline), and some is unintentional (accumulated from evolving requirements or inexperience).
Recognizing Debt
Common indicators include areas of the codebase that developers avoid modifying because changes there frequently cause unexpected breakage, duplicated logic spread across multiple files or services, outdated dependencies that are increasingly difficult to upgrade, inconsistent patterns where different parts of the codebase solve similar problems in different ways, and slow or flaky tests that erode confidence in the test suite.
Managing Debt Strategically
The goal is not to eliminate all technical debt — that is neither possible nor cost-effective. Instead, the team should make debt visible by tracking it in the issue tracker alongside feature work, prioritize debt that blocks current or imminent work, allocate a consistent percentage of each sprint (often 10–20%) to debt reduction, and refactor opportunistically by improving code in the vicinity of active feature work. Refactoring should always be accompanied by tests that verify existing behavior is preserved.
Feature Flags and Progressive Delivery
Feature flags decouple deployment from release, allowing code to be merged and deployed without being visible to users until the team is ready.
How Feature Flags Work
A feature flag is a conditional that checks a configuration value at runtime to determine whether a feature is active. Flags can be simple booleans, percentage-based rollouts (show the feature to 10% of users), or targeted by user attributes (enable for beta testers, internal employees, or users in a specific region). Feature flag services like LaunchDarkly, Flagsmith, Unleash, or even a simple configuration file provide management interfaces, audit logs, and integration with deployment pipelines.
Benefits During Development
Feature flags allow long-running features to be merged incrementally without blocking other work, enable A/B testing and gradual rollouts that reduce blast radius, provide a kill switch that can disable a problematic feature instantly without a rollback, and support trunk-based development by keeping the main branch always deployable.
Hygiene
Stale feature flags add complexity and confusion. Teams should establish a lifecycle for each flag: creation, activation, full rollout, and removal. Flags that have been fully rolled out for more than a sprint or two should be cleaned up by removing the conditional and the flag definition.
Continuous Integration
Continuous integration (CI) is the practice of merging code changes into the shared branch frequently — at least daily — with each integration verified by an automated build and test pipeline.
Pipeline Design
A typical CI pipeline includes checkout and dependency installation, linting and static analysis, unit tests, integration tests, build or compilation, and artifact publishing (Docker image, package, or binary). Each stage should fail fast: linting and unit tests run first because they are quickest, providing developers with feedback in minutes rather than waiting for slower integration tests.
Pipeline Best Practices
Builds should be deterministic, meaning the same commit produces the same result regardless of when or where it runs. Dependencies should be cached to reduce build times. Secrets should be injected via the CI platform's secret management, never committed to the repository. Build status should be visible to the entire team through dashboard displays or chat notifications.
Trunk Health
The main branch should always be in a deployable state. If a build breaks, fixing it takes priority over all other work. Teams that tolerate a broken main branch for extended periods erode trust in the CI process and accumulate integration risk.
Key Deliverables
By the end of the Development phase, the team should have produced a working application that implements the prioritized requirements, a clean and well-documented codebase with consistent patterns, a comprehensive suite of unit and integration tests, a CI pipeline that validates every change automatically, architecture and API documentation, and a codebase that is ready to enter thorough testing and eventual deployment.
The Development phase does not end abruptly — it overlaps with Testing and, in mature teams, with Deployment. The practices described here ensure that the code entering those phases is as solid as possible.