Why You Shouldn’t Use Clean Architecture and DDD Until You Read This

Illustration of a person kneeling before a tall stack of programming books, such as "Clean Architecture," "DDD," "Refactoring," and "Clean Code," as if worshiping them.

Introduction

How do you imagine the code of a true professional? Most likely, it's ultra-clean, following all the "best practices"—a well-defined structure, clear separation into contexts, layers, and domains… Everything is meticulously thought out.

Now, imagine you need to launch an MVP quickly. You decide to implement Clean Architecture, add DDD, and carefully structure your business logic into domains… A month passes, and you have a perfect architecture—but the business still hasn’t received a single working feature.

Is that really a professional approach? Or have you simply fallen into the trap of unnecessary complexity? Let’s dive in.

This article isn’t here to convince you that Clean Architecture and Domain-Driven Design are useless. Quite the opposite—I believe these are powerful tools that every developer should know how to use. However, like any tool, they need to be applied in the right situations. Developers who are just discovering DDD and CA often start using them everywhere—MVPs, simple CRUD applications, and projects that don’t truly need them.

These approaches do solve real problems, especially in mature projects with complex domains. They help separate responsibilities, focus on business logic, gain flexibility, reduce dependency on technologies and third-party services, and make testing easier.

Because of this, many developers assume that every project must strictly follow "best practices" in software development. They believe that this is what defines professionalism—as if you won’t be taken seriously unless your project is divided into domains and layers.

The Illusion of Best Practices

As humans, we tend to live in illusions and enjoy them, like soaking in a warm bath—because it makes the world feel simple and predictable. For example, we might think: "If I carefully design my startup’s architecture according to the “best practices”, it will inevitably succeed. After all, giants like Google, Microsoft, and Amazon use them, and they’re doing great."

But in reality, the world is far more complex and unpredictable. This belief is nothing more than a cargo cult, built on the illusion that only code written according to "best practices" can function well and avoid problems.

I’ve worked at many companies and never noticed a correlation between market success and architecture. I’ve seen startups that, within a year, implemented a microservices architecture with Clean Architecture and DDD—only to have a small team of developers struggling to maintain it, spending more time on architecture than on business logic.

I’ve also seen startups running on a simple Symfony monolith with no CA or DDD, as well as international companies practicing Clean Architecture, DDD, and Event Storming. Yet, those who ignored these approaches entirely—and even ran on outdated PHP versions—were often just as successful.

One thing I wish I had understood much earlier is that end users don’t care about your app’s architecture or the "best practices" hidden under the hood. What matters to them is whether your app solves their problems. If it doesn’t, having a perfect domain model and well-structured layers won’t make your business successful. Architecture should help developers deliver features faster and fix bugs efficiently, not slow down development or create unnecessary obstacles.

Developers also tend to overcomplicate simple things. This is how simple CRUD projects end up overloaded with Clean Architecture, Domain-Driven Design, and other "best practices." Sure, it might look impressive and professional, but in reality, it doesn’t solve any real problems. Instead, it raises the project’s entry barrier and slows development due to the overhead of maintaining multiple layers of abstraction.

Clean Architecture and DDD solve scalability and complexity issues that arise in large projects. However, for small projects with just one or a few developers, these approaches often introduce unnecessary complexity and overhead. In such cases, a simpler architecture can be far more effective, allowing for faster feature development without excessive abstractions. The true value of CA and DDD becomes apparent in large-scale projects, where a big team works on intricate business logic.

The Hidden Costs of Over-Engineering

The hidden costs of over-engineering often go unnoticed when it comes to Domain-Driven Design and Clean Architecture. These approaches offer clear advantages, but they are not always justified—especially for small or short-term projects.

One of the biggest issues is unnecessary complexity, which slows down development. Instead of solving business problems, time is spent maintaining architectural decisions. A large number of abstractions, strict adherence to separation-of-concerns principles, and rigid dependency management can make the codebase harder to navigate rather than easier.

Another hidden problem is the learning curve for new developers. Not everyone is familiar with concepts like Bounded Contexts, Aggregates, or Value Objects, and learning them takes time. This can delay onboarding for new team members and reduce overall efficiency.

Before adopting complex architectural solutions, it’s essential to evaluate whether the added complexity is justified. If a project has an uncertain future or the team lacks sufficient experience, over-engineering can create more problems than it solves.

This is particularly important when working on your own project. If your goal is not just to practice DDD and Clean Architecture but to deliver a working product, excessive complexity will only slow you down. In small projects where you wear multiple hats—designer, frontend and backend developer, business analyst, and copywriter—focus on what matters instead of getting caught up in architectural formalities. Trust me, you'll have enough other challenges without worrying about context separation and unnecessary abstraction layers.

When Clean Architecture and DDD Actually Make Sense

These development approaches wouldn’t have gained such popularity if they didn’t offer significant advantages for developers.

They are particularly useful for applications with complex and extensive business logic, which may be confined to a single domain or span multiple domains. For example, consider a restaurant and a delivery service—each is intricate and substantial on its own. Domain-Driven Design and Clean Architecture help make working with such domains more structured and manageable.

These approaches enable teams to split work into independent domain-focused groups, with one team handling the restaurant domain and another handling the delivery domain. This accelerates development, as each team can focus deeply on their respective domain without having to understand the entire system at once or keep too much context in mind.

"What goes into production is not the domain expert’s knowledge, but the developer’s assumptions about that knowledge."

Alberto Brandolini, author of Introducing EventStorming

Domain-Driven Design also improves understanding of the domain, allowing developers to make more accurate assumptions about how different parts of the system behave. By defining clear Bounded Contexts and establishing a ubiquitous language, DDD helps developers avoid ambiguities and better align the code with real-world business processes. This reduces the risk of misinterpretations and ensures that the system genuinely meets user needs.

A Pragmatic Approach to Architecture

Don’t fight windmills—don’t waste time and effort solving problems that don’t exist. Not only does this drain your resources, but it also makes your application’s logic unnecessarily complex for no real reason, ultimately slowing down development. Good architecture should solve problems, not create them. It’s always better to start simple and refactor when needed rather than overcomplicate things upfront.

It’s crucial to introduce only those levels of abstraction that address real problems you’ve encountered. Let’s look at a real-world example of excessive Clean Architecture abstraction in practice.

An Example of Over-Engineering

// Domain: User model
class User {
    public function __construct(
        private string $id,
        private string $name,
        private string $email
    ) {}

    public function getId(): string { return $this->id; }
    public function getName(): string { return $this->name; }
    public function getEmail(): string { return $this->email; }
}

// Application: User repository interface
interface UserRepositoryInterface {
    public function findById(string $id): ?User;
}

// Infrastructure: Doctrine user repository
class UserRepository implements UserRepositoryInterface {
    public function __construct(private EntityManagerInterface $entityManager) {}

    public function findById(string $id): ?User {
        return $this->entityManager->getRepository(User::class)->find($id);
    }
}

// Application: User use case
class GetUserUseCase {
    public function __construct(private UserRepositoryInterface $repository) {}

    public function execute(string $id): ?User {
        return $this->repository->findById($id);
    }
}

// Infrastructure: User controller
class UserController {
    public function __construct(private GetUserUseCase $useCase) {}

    #[Route('/users/{id}', methods: ['GET'])]
    public function getUser(string $id): JsonResponse {
        $user = $this->useCase->execute($id);
        return $user ? new JsonResponse($user) : new JsonResponse(null, Response::HTTP_NOT_FOUND);
    }
}

And trust me, I’ve seen far worse examples of unnecessary over-engineering. I could add a few DTOs for input and output data, several factories to create them, and even more useless abstraction layers for this simple endpoint.

A Simpler and More Efficient Approach

class UserController extends AbstractController {
    public function __construct(private EntityManagerInterface $entityManager) {}

    #[Route('/users/{id}', methods: ['GET'])]
    public function getUser(string $id): JsonResponse {
        $user = $this->entityManager->getRepository(User::class)->find($id);
        return $user ? new JsonResponse($user) : new JsonResponse(null, Response::HTTP_NOT_FOUND);
    }
}

Of course, this example is somewhat exaggerated, and it might make sense to extract database logic into a repository—since this likely isn’t the only place where users are queried. But imagine how much time you could save by avoiding unnecessary complexity and sticking to a pragmatic approach like this. The time saved could be better spent writing high-quality tests, which bring far more value.

The Best Advice? Find the Right Balance

Don’t jump from one extreme to another—from overly complex, rigid code following DDD and Clean Architecture to code that ignores even basic common sense.

✍️
Every time you consider using DDD or Clean Architecture, ask yourself: What real problem does this solve? If the answer is none, and you’re only preparing for problems that might arise in the future—don’t do it.

Conclusion

Clean Architecture and DDD are powerful tools, but they are not a universal solution to every development challenge. They make sense when the complexity of business logic justifies the added layers of abstraction. However, if your project doesn’t require this level of complexity, applying these approaches will only slow down development, make code harder to understand, and provide little real benefit.

Before adopting these methodologies, ask yourself: "Does my project truly need this?"

Is your business logic so complex that it justifies introducing additional domain concepts and abstraction layers? Will it bring real value, or are you just trying to make the code "look better"?

Follow the principle: "Keep it as simple as possible, but not simpler than necessary."

Start with a simple, clear architecture, and only introduce complexity when a real need arises. Remember—professionalism is not about blindly following trendy practices, but about knowing when and where to apply them effectively.