Implementation

You have your requirements documented, your system designed, and your API contracts agreed upon. Now it is time to build. But implementation is not just “start coding.” It is a disciplined process that separates professional engineers from hobbyists. In this post, we walk through how to turn a design into working software, using our running example: building a task management app (like a simplified Trello).

1. Project Setup

Before writing a single line of business logic, establish your foundation. A clean project setup saves you hundreds of hours over the life of a project.

Set up four things immediately:

  • Project structure — organize code by layer (domain, service, controller, repository) or by feature. Pick one and stay consistent.
  • Version control — initialize a Git repository. Create a .gitignore for your language. Make your first commit the empty project skeleton.
  • CI/CD pipeline — even a basic pipeline that runs tests on every push catches problems before they reach production.
  • Development environment — use Docker or a script so every team member runs the same setup. “It works on my machine” is not acceptable.

A clean project structure for our task management app looks like this:

task-manager/
├── src/
│   ├── main/java/com/taskmanager/
│   │   ├── domain/          # Entities and value objects
│   │   ├── service/         # Business logic
│   │   ├── controller/      # REST endpoints
│   │   ├── repository/      # Data access
│   │   └── config/          # App configuration
│   └── test/java/com/taskmanager/
├── docker-compose.yml
├── Dockerfile
├── .gitignore
└── README.md

This structure tells any developer exactly where to find things. Domain logic is isolated. Controllers are thin. Configuration is centralized. This is not accidental — it is intentional design.

2. Start with the Domain Layer

The most important rule of implementation: build your core business logic first, independent of any framework. Your domain layer should work without Spring Boot, without Flask, without a database. It is pure logic.

Why? Because business rules change slowly. Frameworks change fast. If your business logic is tangled with your framework, every upgrade becomes a rewrite.

Here is the TaskService for our task management app in Java:

@Service
public class TaskService {

    private final TaskRepository taskRepository;

    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }

    public Task createTask(String title, String description, Long assigneeId) {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("Task title cannot be empty");
        }
        Task task = new Task();
        task.setTitle(title.trim());
        task.setDescription(description);
        task.setAssigneeId(assigneeId);
        task.setStatus(TaskStatus.TODO);
        task.setCreatedAt(LocalDateTime.now());
        return taskRepository.save(task);
    }

    public Task getTask(Long id) {
        return taskRepository.findById(id)
            .orElseThrow(() -> new TaskNotFoundException("Task not found: " + id));
    }

    public Task updateStatus(Long id, TaskStatus newStatus) {
        Task task = getTask(id);
        validateStatusTransition(task.getStatus(), newStatus);
        task.setStatus(newStatus);
        task.setUpdatedAt(LocalDateTime.now());
        return taskRepository.save(task);
    }

    private void validateStatusTransition(TaskStatus current, TaskStatus next) {
        if (current == TaskStatus.DONE && next == TaskStatus.TODO) {
            throw new InvalidStatusTransitionException(
                "Cannot move a completed task back to TODO"
            );
        }
    }
}

And the same logic in Python using Flask:

from datetime import datetime
from enum import Enum

class TaskStatus(Enum):
    TODO = "TODO"
    IN_PROGRESS = "IN_PROGRESS"
    DONE = "DONE"

class TaskService:

    def __init__(self, task_repository):
        self.task_repository = task_repository

    def create_task(self, title, description=None, assignee_id=None):
        if not title or not title.strip():
            raise ValueError("Task title cannot be empty")
        task = {
            "title": title.strip(),
            "description": description,
            "assignee_id": assignee_id,
            "status": TaskStatus.TODO.value,
            "created_at": datetime.utcnow().isoformat()
        }
        return self.task_repository.save(task)

    def get_task(self, task_id):
        task = self.task_repository.find_by_id(task_id)
        if not task:
            raise LookupError(f"Task not found: {task_id}")
        return task

    def update_status(self, task_id, new_status):
        task = self.get_task(task_id)
        self._validate_status_transition(task["status"], new_status)
        task["status"] = new_status
        task["updated_at"] = datetime.utcnow().isoformat()
        return self.task_repository.save(task)

    def _validate_status_transition(self, current, new_status):
        if current == TaskStatus.DONE.value and new_status == TaskStatus.TODO.value:
            raise ValueError("Cannot move a completed task back to TODO")

Notice what both implementations share: input validation, clear error handling, and a status transition rule that enforces business logic. The framework is irrelevant. The logic is identical. That is the sign of a well-designed domain layer.

3. Build the API Layer

With your domain layer solid, the API layer becomes a thin wrapper. Controllers should do three things: accept the request, call the service, and return the response. Nothing more.

Here is a Spring Boot controller for the task endpoints we designed in the previous post:

@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody CreateTaskRequest request) {
        Task task = taskService.createTask(
            request.getTitle(),
            request.getDescription(),
            request.getAssigneeId()
        );
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Task> getTask(@PathVariable Long id) {
        return ResponseEntity.ok(taskService.getTask(id));
    }

    @PatchMapping("/{id}/status")
    public ResponseEntity<Task> updateStatus(
            @PathVariable Long id,
            @RequestBody UpdateStatusRequest request) {
        Task task = taskService.updateStatus(id, request.getStatus());
        return ResponseEntity.ok(task);
    }
}

The controller is deliberately thin. There is no business logic here. No validation beyond what Spring handles automatically. Every decision lives in TaskService where it can be tested independently.

4. Version Control Workflow

Code without version control discipline is a liability. Every professional team follows a branching strategy. Here is a practical workflow that scales from two developers to two hundred:

# Create a feature branch from main
git checkout main
git pull origin main
git checkout -b feature/task-status-update

# Make small, focused commits as you work
git add src/main/java/com/taskmanager/service/TaskService.java
git commit -m "Add status transition validation to TaskService"

git add src/test/java/com/taskmanager/service/TaskServiceTest.java
git commit -m "Add tests for status transition rules"

# Push and open a pull request
git push origin feature/task-status-update

# After code review and CI passes, merge via pull request
# Delete the feature branch after merge
git branch -d feature/task-status-update

The key rules:

  • Never commit directly to main. Every change goes through a feature branch and a pull request.
  • Keep branches short-lived. A branch that lives for more than a few days is a branch that will have merge conflicts.
  • Write descriptive commit messages. “Fixed bug” tells you nothing. “Add status transition validation to prevent completed tasks from reverting to TODO” tells you everything.
  • Require code reviews. A second pair of eyes catches bugs, enforces standards, and spreads knowledge across the team.

5. Code Quality Practices

Writing code that works is the minimum bar. Writing code that others can read, maintain, and extend is the real job. Follow these practices consistently:

Use meaningful names. validateStatusTransition is clear. check is not. A good name eliminates the need for a comment.

Keep functions small. Each function should do one thing. If you need to scroll to read a function, it is too long. The TaskService above has four methods, each under fifteen lines. That is intentional.

Follow SOLID principles. Single Responsibility means your TaskService handles task logic and nothing else. It does not send emails, generate reports, or manage users. Each of those gets its own service.

Handle errors explicitly. Notice that TaskService throws specific exceptions: TaskNotFoundException, InvalidStatusTransitionException. Never swallow exceptions silently. Never return null when you mean “not found.”

Write tests alongside your code. Not after. Not “when you have time.” Every method you write should have a corresponding test before you move to the next method. Tests are not optional — they are part of the implementation.

6. Senior Tip

There is a well-known principle in software engineering: “Make it work, make it right, make it fast” — in that order.

Make it work means get a functioning solution that passes your tests. Do not worry about elegance or performance. Just make it correct.

Make it right means refactor for clarity. Clean up names, extract methods, remove duplication. This is where SOLID principles and clean code practices come in.

Make it fast means optimize, but only where you have measured a bottleneck. Premature optimization is the root of countless engineering disasters. Profile first, then optimize the hotspot.

Most engineers skip step two and jump straight to step three. They end up with fast code that no one can read or maintain. Resist the urge. Readable code that performs adequately will always beat clever code that no one can debug at two in the morning.

Key Takeaway

Implementation is where engineering discipline matters most. Set up your project cleanly. Build the domain layer first. Keep your controllers thin. Use version control like a professional. Write code that your future self will thank you for. Follow the mantra: make it work, make it right, make it fast. In that order, always.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *