Clean Architecture: Where SOLID, DDD and Event-Driven Systems Fit — a layered architectural illustration shows a protected business-rule core, modular domain rings, outward event particles, and separate database, API and messaging infrastructure to represent inward dependencies and decoupled follow-up work.

Clean Architecture: Where SOLID, DDD and Event-Driven Systems Fit

A feature request looks small until it crosses every layer of an application. Add a new checkout rule and suddenly an HTTP handler, an ORM model, a pricing calculation, an email notification and a message consumer all need to change together. The problem is not that the folders are wrong. The problem is that the business decision is tangled with the machinery used to deliver and store it.

Clean Architecture is a way to make that tangle less costly. Its central idea is simple: organise dependencies so that business rules do not have to know about a web framework, a database or a message broker. SOLID, domain-driven design (DDD) and event-driven design can support that aim, but none of them is a mandatory layer of ceremony.

Key takeaways

  • Business rules should not depend directly on HTTP, SQL or a message broker.
  • SOLID helps keep individual modules understandable; it does not define an entire system design.
  • DDD is most useful where the business language and rules are genuinely complex.
  • Events can decouple completed facts from follow-up work, but they also create delivery and consistency trade-offs.

Clean Architecture sets the direction of dependency

Think of an application in two broad areas. The inner area contains the decisions the organisation cares about: how an order is priced, when it can be placed, and what counts as a valid refund. The outer area contains delivery details: controllers, database drivers, queues, email providers and framework configuration.

Clean Architecture asks dependencies to point inward. A controller may call a use case, and a database adapter may implement an interface used by that use case. The use case should not import the controller, know a SQL dialect or publish directly to a broker.

That direction matters because infrastructure changes more often than core intent. A team may replace a queue, move from one ORM to another, or expose the same capability through an API and a background job. If the business rule is already tied to those choices, each replacement becomes riskier than it needs to be.

A practical boundary can be modest. A PlaceOrder use case depends on an OrderRepository contract. A SQL-backed repository lives outside the use case and implements that contract. The rule stays testable with an in-memory implementation, while the delivery mechanism remains replaceable.

Applied examples of Clean Architecture are commonly discussed in system-design and microservices contexts; see Red Hat Developer’s practical Clean Architecture example. The useful lesson is not that every application needs more layers. It is that a boundary should protect a decision that would otherwise be dragged around by a technical choice.

SOLID keeps those boundaries small enough to use

SOLID is most helpful when treated as local design guidance. It will not tell you where every module belongs, but it can make the modules on either side of a boundary easier to change.

Dependency inversion is especially relevant here. Rather than making the order use case depend on a concrete SQL repository, let it depend on the small capability it needs. The implementation then depends on the application contract, not the other way around.

The same restraint applies to interfaces. A focused OrderRepository is easier to understand than a broad SystemService that reads orders, sends email, charges cards and emits metrics. Keeping responsibilities narrow also prevents unrelated changes from turning into accidental coupling.

Do not create an abstraction merely because a class exists. Add one when there is a credible variation to isolate: a second payment provider, a test double, a different persistence strategy, or a boundary between business policy and delivery. An interface without a real pressure behind it is often just another place to look when debugging.

DDD makes the inner core speak the business language

DDD is not a requirement for Clean Architecture. It becomes valuable when the difficult part of the software is understanding the business, not displaying or saving records. In those cases, code should use the language people use when they make decisions.

For an ordering domain, that might mean Order, Money, PlaceOrder and PaymentAuthorised rather than generic names such as OrderData and ProcessRecord. The point is not stylistic purity. Clear names let developers, product people and operations staff discuss the same decision with fewer translations.

A bounded context is a useful reminder that a word can mean different things in different areas. “Customer” in billing may have different rules and data from “customer” in support. An aggregate is a smaller consistency boundary: a group of related state that one business operation changes according to its own rules. A value object such as Money can keep currency and arithmetic rules together instead of scattering them across handlers.

This level of modelling is worthwhile for volatile, business-critical rules. It is often unnecessary for a stable internal CRUD screen. A good test is whether the team regularly debates the meaning of a term or the validity of a decision. If it does, modelling that language can remove recurring ambiguity. For an applied discussion of the relationship, see Clean Architecture and DDD in Practice.

Events connect completed decisions without direct coupling

Now continue the order example. Once an order is accepted, inventory may need to reserve stock and an email process may need to send a confirmation. The ordering use case should record the completed business fact, OrderPlaced. An outer adapter can later publish it to the chosen broker.

result = placeOrder(command)

if result.accepted:
    record(OrderPlaced(orderId=result.orderId))

return result

The important separation is that the use case records a domain-relevant fact; it does not call a vendor-specific queue client. A publisher outside the core can translate the recorded event into a transport message. That keeps the business rule from becoming dependent on the broker and leaves room for reliable delivery patterns appropriate to the system.

Events are not free. A consumer can receive a duplicate, receive a message later than expected, or observe related messages in an unexpected order. Once work moves across an asynchronous boundary, a team also needs visibility into failed processing and a clear answer to which data is immediately consistent. Treat these as design responsibilities, not edge cases to promise away with “exactly once.”

Start with a modular monolith, then earn more distribution

The safest path is usually not to split immediately into services. Keep modules in one deployable application while making their dependencies explicit. You get many of the testing and clarity benefits without adding network failure, distributed tracing and independent deployment to every feature.

  1. Name one business use case that changes often or causes confusion.
  2. Move its rule and data contract behind a small, framework-independent boundary.
  3. Test that use case without HTTP, SQL or a broker.
  4. Add a domain event only when another process needs the completed fact.

This progression keeps architecture proportional to evidence. If a module later needs different scaling, reliability or ownership, the seam already exists. If it does not, the team has avoided paying the operational cost of a distributed system for a problem it does not have.

The trade-off is deliberate seams

Every boundary introduces some cost: interfaces, mappings, tests, diagnostics and decisions about where a responsibility belongs. Events add monitoring and eventual-consistency questions. DDD adds time spent learning the domain and maintaining a shared vocabulary.

Choose a boundary when it protects something that changes independently, has a distinct business owner, or needs a different reliability or scaling profile. Avoid a boundary when it only mirrors a fashionable diagram. The goal is not an impressive number of layers; it is a codebase where the next meaningful change has a clear home.

Verdict: use the smallest structure that protects the rule

Clean Architecture is a strong fit for teams maintaining long-lived software with complex rules, multiple integrations or a domain that changes under real business pressure. SOLID can keep its modules honest, DDD can clarify the important language, and events can coordinate work that should not be directly coupled.

It is a weaker starting point for small, stable CRUD applications or teams that cannot maintain the additional seams. In those cases, straightforward code with good tests is usually more valuable than a full vocabulary of architectural patterns.

The safest first action is to choose one volatile business use case, write a framework-independent test for it, and introduce only the boundary needed to make that test simple. Design for the next meaningful change, then let the system earn its additional structure.

Leave a Comment

Are you human? Please solve:Captcha


Alpesh Kumar
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.