The Simple Pattern Behind Scalable Software
Chapters6
Business rules live in the domain layer; domain objects are pure and should not depend on outer layers, ensuring core logic stays isolated.
Clean architecture with domain, contracts, application, infrastructure, and presentation layers makes scalable software easily adaptable to change, especially when pairing with AI-driven development.
Summary
HashLips Academy’s video lays out a clear, layered approach to building scalable software. Chris walks us through the onion-like architecture, stressing that the core domain and business rules live in the heart of the system while outer layers expose infrastructure and entry points. He emphasizes that domain objects should be pure and depend inward, not on outer layers. Contracts define blueprints (interfaces) like PaymentProvider and PaymentRepository, decoupling what needs to be done from how it’s done. The application layer orchestrates use cases (e.g., creating and confirming payments) by wiring in contracts via dependency injection. In infrastructure, actual implementations like Stripe or an in-memory provider realize those contracts, allowing seamless swapping without touching the core business logic. Finally, the presentation layer exposes the system to the outside world, proving that you can switch providers (Stripe vs in-memory) without changing the domain. Chris also teases an agent skills capability architecture to help structure code for LLM-assisted development. His takeaway: design first, implement later, and keep the core pristine so future changes are effortless—even when AI is involved in building apps.
Key Takeaways
- The domain layer is the heart of the system, containing business rules and pure domain objects that depend only on inner layers like payment status.
- Contracts define blueprints (interfaces) such as PaymentProvider and PaymentRepository, enabling dependency on capabilities rather than concrete implementations.
- The application layer coordinates use cases (e.g., create payment, confirm payment) by wiring in contracts and domain objects, forming a blueprint for behavior.
- The infrastructure layer implements the contracts (StripePaymentProvider, InMemoryPaymentProvider) and can be swapped without touching domain logic.
- The presentation layer (e.g., HTTP/GraphQL controllers) triggers use cases and demonstrates easy provider swapping in real-time (Stripe vs in-memory).
- Dependency injection is the mechanism that makes layering and testability possible by supplying the required contracts and providers to use cases.
Who Is This For?
Essential viewing for software engineers and AI developers who want to understand how to structure code for maintainability and testability, and who plan to use AI to generate or scaffold applications without breaking core business rules.
Notable Quotes
"The core of our system where our objects live, these objects can depend on each other and they need to be pure. They cannot depend on any outer layer because the dependencies need to point inward."
—Explanation of why domain objects must be pure and inward-facing.
"Contracts are just those blueprints."
—Defining contracts as blueprints that decouple needs from implementations.
"This is where the magic behind clean architecture and dependency injection lies."
—Highlighting the transformative role of DI in layering.
"We can easily swap this provider for the Stripe payment provider."
—Demonstrating provider swap without touching core logic.
"An in-memory payment provider is used for local testing to mock real implementations like Stripe."
—Showing practical, test-friendly infrastructure options.
Questions This Video Answers
- How does clean architecture keep business logic independent from infrastructure?
- What is the role of dependency injection in layering a software system?
- What is a payment provider contract and how does it enable swapping Stripe for a mock implementation?
- How can I design a system so AI-generated code conforms to a clean architecture pattern?
- What are practical examples of domain, application, and infrastructure layers in a payment flow?
Clean ArchitectureDomain-Driven DesignDependency InjectionLayered ArchitecturePayment ProcessingStripeIn-Memory ProviderHashLips AcademyAgent SkillsLLM-assisted Development
Full Transcript
Eventually, when you have done software development for quite some time, you start gravitating towards a cleaner architecture, which I'm going to show in this video. I think it's super important in the age of AI where everyone's building applications to at least prompt the AI to build applications with this architecture so it's easily adaptable for future use. Every developer is different and implements this type of architecture in their own unique way. Now here is an example of what it looks like when we develop software. You can think of uh software development as this onion that you cut in half with different layers and where the core layers are some of the most important layers because the first three layers are basically the business itself and how it operates.
The rest is infrastructure and entry points. So, we're going to discuss this and at the very end, I'm going to actually show you a skill that I've made that can help you when you prompt for your AI to produce clean code like this. But first, let's understand the power of clean architecture. So, the first one is the domain layer. This layer is the heart of your system. This is where the business rules should live. And if you were building a payment system, this is where you'll define things like what a payment actually is, what state can it be in, and what rules must be met for this payment to be created.
And because this is the core of our system where our objects live, these objects and these files can depend on each other and they need to be pure. They cannot depend on any outer layer because the dependencies need to point inward. Here I've got a small example that's going to help us understand the full architecture today. If we open up the domain folder here, you can see the payment uh file as well as the payment status. And the domain objects can sometimes be very simple such as just specifying a string for a payment system or it can be more involved being an object how a payment looks like.
How can we create a pending payment? What is the business rule that goes with creating a pending payment? And so these are our domain objects and is what the business application is all about. Right? So these are your core types of the application. And again very important look at what it's depending on. This payment is depending on payment status. It's not depending on anything in the outer folders over here. making it pure and unaffected by the outer layers. And now we get to the contracts layer. Now basically with this layer is it defines blueprints, right?
Contracts define what the application needs without caring on how those needs are fulfilled. So for example, if we are a payment company, we care about charging for a payment and saving it somewhere. You know, this is where interfaces and abstractions start becoming valuable because rather than depending directly on implementations, we're depending on capabilities. We don't care how that is going to be implemented later on. Contracts are just those blueprints. And so for the example, if we open up the contracts folder, we can see that we've got a payment provider and a payment repository. Okay. So if we open one of these files, you'll see it's an interface and it simply says that we need that capability to save and to find by ID.
Whenever we have stored something, we need to be able to retrieve it. For the payment provider, we have the ability to charge. And here you can see this is just the blueprint, just that interface that tells us what does it need to take in and what is it going to return. And that's all that our contracts are going to define. Contracts can depend on other contracts and also on the domain level. That is fine. But it shouldn't really depend on any outer layers other than itself and the domain. And now we can move on to the application layer.
Now this layer uh is there to coordinate use cases of a system. In our case for the payment system that we are creating, we look at user actions such as creating a payment and how the orchestrated steps uh work. You know what is required to make that payment happen. It uses the domain for business rules and then also contracts for external capabilities like saving or making that payment. Now again this is not the implementation yet. This is almost like a blueprint but it is more like an orchestrator and it orchestrates how that action is going to take place.
Now to better understand this let's go ahead and open up the application layer. Now here we've got a create payment uh as well as a confirm payment. So if you look at the confirm payment use case, you can see that for the dependencies, we actually pass in our contract and the contract is simply a payment repository because inside of this function, we are going to eventually go and find by ID, right? We're going to find that payment. And so the crucial thing to understand here is just simply that this thing itself is illustrating how something is going to happen by using a blueprint because we know what that blueprint contains.
And then when it executes this it can trust that there would have been an implementation done right and passed in. And so you can see all of these are setting up a structure of an application much the same as the create payment in this case. There's a lot that happens, right? But firstly, we pass in for the dependencies. We pass in the payment repository and a payment provider. And then we've got these nice functions where we can create a payment. Um goes in and then creates the pending payment and then continues. Now again uh from the application level what you really uh depending on is the domain and the contracts and this brings us to the most important concept here and that is called dependency injection.
We are injecting frameworks blueprints to say to this use case that we will implement something that will have the capabilities of this provider or of this repository. And if you understand that you are 90% there to understanding how clean architecture works. And the last thing I want to say about the application layer is that up until this point our application is essentially done. The framework of it is essentially complete. Right? The use cases are there. The contracts are set up. The domain is there and existing. And now comes the actual implementation part that follows next when it comes to infrastructure which we're going to discuss in a second.
But I want you to understand that uh when we develop programs we can already preemptively set up the capabilities that we know is needed for the flow and implement them later which you'll see now. And now we can look at the infrastructure layer. So basically this is where the outside world starts showing up in databases and payment providers and external APIs and file systems. So this is the layer where we implement what we've set out as blueprints in the contracts. So the contract that we set up on how a repository should work, we can now actually implement that repository or implement that payment provider like a Stripe payment provider or a PayPal payment provider.
Back in the code, we can see how that looks like. If we go to infrastructure, we can see that we have a Stripe payment provider, which is not the actual implementation. We are just logging out to showcase and prove a point. But you can imagine if we were to include Stripe's SDK and we mention that this needs to adhere to our payment provider contract and guess what our contract says that we need to be able to charge, right? And so we implement the function and we can implement the custom logic to Stripe in here. Same with PayPal or in our case here to test locally we can do an in-memory payment provider and we can just flesh it out and again just logging this out.
Now although these don't have the actual implementations in the example here but this is where the implementations do happen and this means that you can have many different implementations of a payment provider. Maybe you have a QA or staging setup and you need a staging payment provider so that you can test um some logic instead of actually making some payments, right? And so this is just the layer the infrastructure where we start implementing the contracts in the contracts layer. And when it comes to dependencies, we can depend on the contracts as well as the domain types inside of the infrastructure because our dependencies needs to go and point inward.
And now lastly, we get the presentation layer. This is simply how the outside world interacts with our system. Whether it's through HTTP request, GraphQL, websockets, a CLI, or even a UI, it doesn't matter. It's basically the trigger that triggers the application's use cases. And in our code example, if we open the presentation layer, we can go to the payment controller. And here we set up a simple payment controller. And notice how cool this part really is. We create a payment by using the use case. And remember the dependency injection. Well, here we create a new in-memory payment repository and an in-memory payment provider.
Plugging it in here, but we can super easily swap this out, this provider for the Stripe payment provider. And now we using Stripe. And that is the magic behind um clean architecture and dependency injection and working with things in this layered approach because it really allows for the infrastructure to change without the core functionality of our app to change at all. And so if I run this application right now and I head over to the application's beautiful UI and I create a payment, we can see that all these payments are done with the Stripe payment provider.
But we can easily go back and again switch this back to the in-memory provider. Save this. And back in the application, if we create payments now, it's using the in-memory payment system. And so it shows you how easily things can be switched out. And so at the end of the day, like I said, developers implement this sort of infrastructure in different ways. But the idea here is to keep things um clean and so that we get the benefits of modularity, loose coupling, testability, replacing the infrastructure and still protecting our domain. And so here's a little diagram that explains that.
But at the end of the day, whether you understood what I explained or it maybe kind of piqu your interest a little bit, coding is all about learning how to implement these proven patterns over time. Now, if you are someone that deals with a lot of AI or uses AI and LLMs to create applications, maybe this is something that you don't really need to be concerned about at first. But as you go on and as you build something really amazing and you want developers to actually work on it, it's extremely important that the underlying code and how the implementation is done takes these considerations in mind.
And so I have set up some agent skills for myself and I've been covering this repo for quite some time on my channel. If you don't know about agent skills, highly recommend checking out the previous videos where I implement these skills and use them. But if you go to this repo, I'll leave the link in the description and you head over to the capabilities architecture skill. It is a general skill that can be used when you do software engineering and to help you along to at least get your code structured in a very nice way.
And so I use this when I build uh clientside applications or when I go and develop APIs so that I know that my applications are nicely structured or at least considered by the LLM to use things like dependency injection and clean architecture. So I hope this video helped a lot. I will make a follow-up video at some point where I use the capability architect. But I just wanted to share this with you and I hope that this can be of some value. Maybe you learned a thing or two. Who knows? Let me know in the comments.
And yeah, until next time, have an amazing day. Yes.
More from HashLips Academy
Get daily recaps from
HashLips Academy
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.









