Pragmatic Clean Architecture in Python: A Conversation with Sam Keen
Applying DDD and the dependency rule in real-world Python—keeping frameworks at the edge, modeling with dataclasses/Pydantic, and using AI without breaking clean boundaries.
From structuring APIs and isolating domain logic to refactoring legacy systems, Python’s flexibility presents both opportunities and challenges for building sustainable software. In this conversation, we speak with
—author of Clean Architecture with Python (Packt, 2025)—about applying architectural principles to real-world Python projects without sacrificing the language’s ethos.Sam is a software engineering leader with over 25 years of experience, a polyglot developer who has used Python everywhere from early-stage startups to large-scale systems at AWS, Lululemon, and Nike. At Lululemon, he led the company’s first cloud-native development team, setting foundational standards for distributed architecture. Currently a Principal Engineer at Pluralsight, he focuses on leveraging generative AI for software engineering enablement—building tools that amplify developer productivity while preserving architectural integrity.
In this interview, we explore how clean architecture can be adapted to Python’s dynamic nature, where SOLID principles prove tricky, and how to keep frameworks like Django and FastAPI from leaking into core logic. We also discuss pragmatic strategies for enforcing the dependency rule, modeling entities and value objects with Python’s modern features, and managing testing and refactoring in complex systems. Looking ahead, Sam offers insights on how AI is reshaping development workflows and what it means for applying clean architecture across services and scaling applications.
You can watch the full conversation below—or read on for the complete transcript.
1: What motivated you to write Clean Architecture with Python, and why do you think Python as a language needs its own treatment of these ideas?
Sam Keen: I’ve been in development for quite some time—and in Python for a big portion of that. As for the topic, clean architecture has been around; Uncle Bob’s book has been out for quite some time. I’d always seen it discussed in the context of static languages like Java and C#. In a lot of Python communities, the thinking was that we didn’t quite need that—that it seemed overburdensome. So I wanted to take an approach and see: can we take from clean architecture the aspects that help us maintain larger Python codebases—without trying to turn it into Java? I knew there would be pushback, so I definitely wanted to keep it Pythonic.
As for why write a book—this is my first published book. During the COVID years, I had a lot of time and got into doing YouTube tutorials for game development. I really liked that content-creation process, and when the opportunity came up to write this book, I thought, yeah, that’s the next step. I’ve always wanted to write a book, so those motivations aligned with good timing.
2: In your book, how do you explain clean architecture in Pythonic terms—especially to developers who know the classic concepts but struggle to apply them cleanly in real-world Python projects?
Sam Keen: Kind of returning to the previous answer: clean architecture aligns well with the Pythonic ethos—one of Python’s core features is to be explicit rather than implicit. Clean architecture gives you that map for your application. If you’re doing object-oriented development, the first things you’re concerned with are the classes—how to design those classes. I’m sure we’ll talk about SOLID. You use those SOLID principles on the class itself to ensure it’s cohesive. Clean architecture takes that and expands on it—how do we apply some of these same principles to the entire application as a whole? The original “clean architecture” was explicit that it’s not a framework and not a rigid set of rules; it’s a set of principles to follow and adapt to your needs. There isn’t one playbook for clean—it depends on what you’re building. You don’t want to build a skyscraper if it’s a little cottage codebase.
3: SOLID principles are often cited as foundational to clean architecture. According to you, which of them are easiest and which of them are the hardest to apply in a dynamic language like Python?
Sam Keen: I think it’s kind of the usual suspects. With SOLID, the S—the single responsibility principle—a lot of folks comprehend pretty well. It’s the first letter, and people understand that a class should have a single concern, that sort of thing. That translates well into Python. The compiler isn’t going to help you much in that regard—it’s more of a human design decision.
It gets more into the nuance with the I—the interface segregation principle—having well-structured and focused interfaces. That’s where it gets tricky. For instance, you might have a vehicle class. You wouldn’t want to have the engine be part of that vehicle base class, because you may have gasoline cars but also electric cars. If you couple the concept of an engine directly into the vehicle base class, you’ll end up with a class that has a power level and a fuel level in liters. You’ll have parts of that interface that don’t make sense for all of the concrete classes that inherit from it.
Another one is the L—the Liskov substitution principle. This is about ensuring that if you have a base class and then classes implementing it, none of those subclasses disrupt the contract of the base class. Anywhere in your code where you’re referring to the base class, you should be able to insert one of the child classes and have it function fine. That’s something a compiler in a static language would help with. In Python, you don’t have that, so type hinting—and I’m sure we’ll talk more about this since it’s core to the book—paired with mypy gives you a little bit of that compiler-like type checking. That can be very helpful in Python.
And then of course, unit testing is always good. So the L and the I are the tougher ones, while S is the easiest.
4: How do you enforce the dependency rule in your projects so that business logic stays free from frameworks, ORMs, or infrastructure code?
Sam Keen: The dependency rule is really core to clean architecture. If you get that one thing right, you’re doing quite well. Conceptually, clean architecture is built in layers. You have the inner domain layer—core business objects with very few dependencies. Then you move out to the application layer, which orchestrates workflows and manages those entities—for example, “save task” or “complete task.”
Then there’s the interfaces layer. It should be a thin layer, often with controllers that translate between the outer layer and the inner business core—the application and domain layers. Finally, the outer layer is your frameworks and drivers. That’s where all the volatility is—things like SQLAlchemy and external dependencies that you don’t control.
Back to your question: how do you enforce the dependency rule? The principle is that dependencies all face inward. The domain layer shouldn’t be aware of anything above it. The application layer shouldn’t be aware of anything above it. They should only depend on what’s inside.
In Python, just like in other languages, you can check this. One way is pragmatic—make sure the team understands the principle and why it matters. Another way is structural—use a folder structure: a folder for domain objects, a folder for application objects, and so on. That way, each file is bound by the rules of its layer.
You can also get precise with automation. For example, in the book we give a simple fitness function test. It runs linting across import statements and checks them against the known directory structure of the layers. If it finds that a use case in the application layer is importing from, say, a driver, it fails the build. That shifts knowledge of violations left, so developers can correct them quickly.
So, it’s a hierarchy: first ensure the team understands the principle, then reinforce it with clear folder structure, and finally back it up with tests that assert violations.
5: What kind of project structure do you recommend? Do you prefer separation by layer—domain, interfaces, etc.—or by feature? And how do you keep the layout from becoming overly rigid?
Sam Keen: One example we mentioned earlier is having a folder per layer—that’s definitely a possibility. But to step back, the bigger principle is: always have the simplest solution that meets the needs of your project and your team. You don’t want to overbuild.
For example, you might have a very simple CRUD application, essentially an API fronting a database with create, update, delete functionality. There’s not much to it. In that case, you could build it in more of a feature structure—say, a task microservice—and just go with FastAPI in a one- or two-file implementation. That makes sense because there isn’t much domain logic in that particular service. I see a lot of single-file frameworks that work quite well for these small cases. That’s one end of the spectrum.
On the other end, as projects and codebases grow larger, with more complex business rules, you start to shift toward the domain structure we talked about earlier—a folder per layer. That helps put in a structure where the right thing to do is also the easiest. For example, if your team decides to support multiple users instead of just one, you now need a User domain object. With a folder-per-layer design, the map is already there: the business object goes in the domain folder, the orchestration goes in the application folder, and so on.
It’s not one-size-fits-all. It’s something you can evolve into. And when your team makes an intentional decision to bend a common practice—for pragmatic reasons—document it in an ADR, an architectural decision record. That way it’s explicit and intentional, not accidental complexity. It also helps developers who come later—or even yourself six months down the road—understand why that choice was made.
6: Domain-driven design is a big part of your approach. How do you model things like entities and value objects in idiomatic Python?
Sam Keen: Something very popular in modern Python is the use of dataclasses. They’re a great way to model domain objects because they eliminate boilerplate and make classes very easy to comprehend—you see just the attributes and functions.
For entities, which have an identity, I use a thin base entity class that every entity extends. A Task, a User, any entity extends this base entity class. That gives you a primary ID field—a reserved field for all entities. Anything in the system that knows it’s dealing with an entity knows that ID field is there and that it’s universally unique.
In Python specifically, in that entity class you’d also implement the __hash__
and __eq__
methods. That keeps you in line with the concept of an entity having identity. You can change all the attributes of that class, but it will always represent the same person or the same task—it’s just that its attributes have changed.
A value object is different. It doesn’t have an ID field; it’s defined solely by its properties. Again, a dataclass works well, but here you’d set frozen=True
to make it immutable. For example, an exchange rate could be modeled this way. If any of its attributes change, it’s no longer the same object. By making it immutable, you guarantee that once an exchange rate object is created, its values won’t change, which fits the definition of a value object.
So, as a Python developer, the tools for following domain-driven design are built into the language. It aligns well with the principles of clean architecture.
7: Frameworks like FastAPI or Django can easily creep into core logic. How do you recommend engineers keep frameworks out of the domain and use case layers?
Sam Keen: Again, it builds on what we talked about—deciding on the directory structure that makes sense for your project. You want to make the right thing to do the easy thing. A clear structure gives you context: when you look at a class in the application layer, it should be obvious if it’s behaving abnormally by depending on a framework.
Beyond structure, it comes down to applying principles and patterns. Take databases, for example. SQLAlchemy is a framework. You shouldn’t see any reference to it in the domain or application layers. The anti-pattern would be a User class in the domain layer with SQLAlchemy methods directly on it to save to the database—that’s direct coupling.
Instead, you use the repository pattern. You define an interface with simple methods like save_user
, get_user
, delete_user
. Your User object depends only on that contract. Then, in the frameworks layer, you implement that repository using SQLAlchemy or whatever tool you need.
That way, the domain isn’t directly coupled to the framework—both the domain object and the implementation simply agree to the interface. This is the general pattern across the board: keep frameworks out of your core logic by making them details implemented at the outer layers.
8: Do you use Pydantic or dataclasses in your domain models, or do you restrict them to boundaries? How do you handle input validation and transformation cleanly?
Sam Keen: That’s an interesting one. When I was writing the book, I actually drifted into being too strict with clean architecture—treating it almost like a framework. In some early drafts, I found myself duplicating property validation in two places: once in the interface layer and again in the domain layer, but using different mechanisms. Anytime you’re duplicating validation, that’s a red flag.
The framework I was using was Pydantic, which is very popular in Python. I use it extensively. It has strong validation and serialization methods. In practice, I made a calculated choice: in some applications, I would allow Pydantic into the domain layer. That’s because it’s mainstream, well supported, and it reduced a lot of boilerplate and duplicated code in that specific case.
That’s the bigger point—clean architecture is a set of principles, not a rigid framework. Sometimes you’ll make compromises to reduce complexity, and that’s OK. The important thing is to be transparent about it. Document the decision in an ADR—an architectural decision record—so it’s clear to the team and future developers that it was a conscious, intentional choice. That way, you’ve managed the trade-off explicitly rather than letting accidental complexity creep in.
9: What’s your testing strategy for a clean architecture codebase? You mentioned unit testing and things like that a bit earlier, but where do unit, integration, and end-to-end tests fit within this concept?
Sam Keen: A very common approach to testing is the test pyramid—Martin Fowler and others have popularized this. You want the base of that to be unit tests, which just test individual functions. They’re very quick—testing the behavior of a class on its own—so you want a large number of those. Above that are integration tests, where classes work together; in clean architecture, that often means classes working across layers. At the very top are end-to-end tests, where you actually boot up infrastructure and test as a real user against the application. Those are slow and often brittle because they test interfaces that change quite a bit, so maintaining many of them is a burden.
If you have a tightly coupled application, it’s hard to implement that pyramid—you can end up with an “ice cream cone,” because unit tests are hard to write and you brute-force a lot of end-to-end tests. Clean architecture enables you to have the true pyramid. The domain layer has no dependencies, so you can easily write unit tests without starting a database or worrying about network calls. Up into the application layer, because you’ve used dependency inversion and coded use cases against interfaces rather than concrete classes, a test can insert a mock that implements the interface. That keeps those tests very quick as well. You still need end-to-end tests for critical workflows as final validation, but the confidence comes from a plethora of fast unit tests and some integration tests. Clean really enables that true test pyramid.
10: What’s your approach to refactoring legacy Python code? How do you introduce clean architecture there without overhauling everything at once?
Sam Keen: We have a chapter devoted to this, and much of it is common practice regardless of language. What you don’t want to do is a Big Bang release—rewriting the entire stack and trying to release it all at once hardly ever works. It takes too long, requirements change, and the system you’re rebuilding keeps changing. You have to figure out how to slice up the problem. The “strangler fig” is a common metaphor: it’s a fig vine that grows up and engulfs a tree.
Clean helps because you’re going from a system without a map—something chaotic—to one that has a map and discrete components. That lets you take Service A and split it off into a clean architecture with full test coverage. Where to start: begin at the lower layers. Look at your legacy system and find all the parts that in aggregate build the concept of a user. Extract that domain logic and build your true domain User in clean architecture. Then, using gateways and feature flags, parallelize traffic to the old system and the new system, compare, and ensure parity in state changes for the user across both.
Beyond domain objects, look for natural bounded contexts—returning to domain-driven design—and extract them into domains with their use cases. Do everything you can to avoid a Big Bang release. Clean architecture gives you the guidance to restructure incrementally and release with confidence.
11: How is AI changing the way we approach clean architecture?
Sam Keen: AI is the definition of disruptive. This generative AI wave we’ve come in on is really interesting because, again, we started the book a little over a year ago and, in AI years, that’s forever ago. I think baby ChatGPT-3 was coming out or something, and LLMs were just starting to be able to build Snake—that was the extent of what they could build. And then you look at where we are now.
There are a couple of dimensions. If you’re thinking of AI as a feature you would add to an application—integrating an LLM into an application—nothing really changes. That’s a driver—a framework driver. So the knowledge of, say, LangChain or LlamaIndex—really common frameworks—that’s all going to stay in the outer layers. Same playbook, and then you integrate that down to your pure domain objects. So that part doesn’t change.
The other part is using AI to build with—coding tools and these sorts of things. It helps. We talked about writing tests—AIs are great at writing unit tests, so you can definitely leverage it there. That’s where you mitigate hallucination—you’re writing tests to validate, so you can know the AI is doing the right thing. Another example: we have that User that needs to be saved to a database, and it has an interface contract. You can use AI to build the concrete class against that interface—at least get a start on it. Some might think of that as boilerplate, but it can do that.
Overall, the advantage is this idea of “context engineering.” The AI’s ability to help is only as good as the context you give it. If you let it index legacy, tightly coupled systems, it’s not really sure what the plan was—there kind of wasn’t one—so the AI will continue to build against that codebase without much of a plan. Whereas, if you’re explicit about your approach and you have these four folders with easily defined rationale of what goes into each folder, all that context goes to the LLM—since it’s helping a human. Using clean architecture and having that playbook for how to build out your application helps AIs do the right thing and not go off the rails. It’s exciting.
12: How do you apply clean architecture across multiple services or modules? Should each follow the full layering pattern or do you recommend something else?
Sam Keen: We touched on this a little bit. It’s not one pattern to fit everything. With multiple services, it’s the same idea. The purpose—under the context that what you’re building is multiple services—matters. You may have portions that are really straightforward CRUD applications. You just know that you don’t want direct database access, so you put an API in front of that. Maybe, at this point in time, it’s kind of one-to-one mapping—you have it for defense if you need to change it in the future. In those cases, FastAPI and using Pydantic throughout might be the right mechanism.
But your larger, business-rule-centric, orchestrating parts of the application—those are where you invest in a fully layered approach. And even though you’ve built services, you treat them as framework drivers with respect to one another. You have Service A you’ve built; you have Service B you’ve built. Service B treats Service A as a detail and doesn’t let Service A’s implementation details get pulled into the deeper layers of Service B.
13: When building scalable applications, how do you decide what belongs in the core versus what stays at the edge—for example, pagination, caching, or authentication?
Sam Keen: Yeah, so that—again, there’s a little bit of thought process to that, of course. You kind of get into that domain-driven design mentality of what’s core to the domain. So a user—what’s core to a user—versus, like, transport protocols and these sorts of things. Those are just details—computer concepts.
A tricky kind of heuristic is: what makes sense even if you weren’t a computer program? To explain that—like, a user having a schedule is a concept that makes sense even before computers were invented. But the knowledge that we’re transferring using JSON or gRPC—that’s a computer concept; it’s a detail of the platform we’re implementing these user domain objects on. To be facetious, if we switch to quantum computers in ten years, we’ll bring our domain objects with us, but all those other details are going to change.
So that’s the macroscopic way to think about it—what are the nouns, the objects of our system—versus what’s a transport or technology detail that’s not core and should stay at the edge. There is some nuance to authentication. You may have a system that needs to be impenetrable, so every layer may need to validate the authentication—like a zero-trust approach. But you may not be in that case, so you may stop authentication at, say, the adapters layer. Then everything below either assumes authentication or doesn’t have knowledge of it. That’s an example where that computer concept could come all the way down to the domain out of need.
14: How do you manage inter-service communication in Python systems built with clean architecture?
Sam Keen: That’s—again, that’s a common practice regardless, but clean helps you with it. In event-driven architecture, messages are another way you can leak implementation details. Be very cognizant of the information you put in the message. It should be past tense—“this happened”—those sorts of practices.
Be cognizant that it’s a common way for implementation details to get transmitted across the wire, versus, you know, a leaky interface at the code layer. Even at the transport level, you can leak implementation details that you don’t want to, and that will cause coupling as well.
To learn Clean Architecture through a series of real-world, code-centric examples and exercises, optimize system componentization, and significantly reduce maintenance burden and overall complexity, check out Clean Architecture with Python by Sam Keen. The book helps you apply Clean Architecture concepts confidently to new Python projects and legacy code refactoring.