Skip to main content
    Courses/Java/Clean Architecture

    Lesson 49 • Expert

    Clean Architecture

    Structure a Java application so its business rules stay pure and testable — by splitting it into layers, pointing every dependency inward, and hiding databases and frameworks behind ports and adapters.

    What You'll Learn in This Lesson

    • Split an app into domain, application, infrastructure & presentation layers
    • Apply the dependency rule so source dependencies point only inward
    • Model entities (identity + behaviour) and value objects (immutable)
    • Define ports (interfaces) and adapters (implementations) — Hexagonal style
    • Write use cases / interactors that orchestrate one workflow each
    • Use dependency inversion to keep frameworks out of your domain

    Before You Start

    This lesson builds on Interfaces & Abstraction (ports are interfaces), OOP Design Patterns (dependency injection), and Unit Testing (the payoff is testable code). Comfort with Generics helps too.

    A Real-World Analogy

    Think of your application as a house with standard wall sockets. The wiring inside the wall is your domain — the real logic, hidden and stable. A socket is a port: a fixed, agreed-upon shape that says "plug something in here." A lamp, a kettle, or a phone charger is an adapter: it conforms to the socket and provides the actual behaviour.

    You can swap the kettle for a toaster without rewiring the house, because both speak "socket." In the same way you can swap a JpaOrderRepository for an in-memory fake, or Stripe for PayPal, without touching a single business rule — as long as they plug into the same port. The wiring (domain) never reaches out to grab a specific appliance; the appliances plug into it. That is the dependency rule made physical.

    1️⃣ The Four Layers & the Dependency Rule

    Clean Architecture sorts your code into concentric layers — picture an onion. At the centre is the domain: pure business logic that knows nothing about databases, the web, or any framework. Around it sits the application layer (your use cases). On the outside live infrastructure (databases, message queues, third-party APIs) and presentation (controllers, DTOs).

    One rule holds the whole thing together — the dependency rule: source-code dependencies may only point inward. An outer layer may import an inner one, but an inner layer must never import an outer one. Your domain therefore never mentions Spring or JPA. That is what lets you test business logic with no framework, swap your database without rewriting rules, and read the domain to understand the system.

    LayerContainsMay Depend OnExample
    DomainEntities, Value Objects, EventsNothing!Order, Money, OrderId
    ApplicationUse cases, Ports (interfaces)Domain onlyPlaceOrderUseCase
    InfrastructureDB repos, API clients, messagingApplication + DomainJpaOrderRepository
    PresentationControllers, DTOs, view modelsApplication + DomainOrderController

    2️⃣ The Domain: Entities & Value Objects

    The domain layer holds two kinds of object. An entity has a stable identity that outlives its data: two Orders with the same OrderId are the same order, even if their contents differ — so you compare them by id. A value object has no identity; it is defined entirely by its values, so two Money of USD 10.00 are interchangeable and equal.

    Crucially, these objects carry their own behaviour and rules. Order.complete() refuses to run twice; Money.add() rejects mismatched currencies. An object that is just fields with getters and setters — with all the logic living in some service — is called an anaemic domain model, and it is exactly what Clean Architecture avoids. Put the rules in the object that owns the data.

    Worked Example — Domain Layer (Entity + Value Object)
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.ArrayList;
    import java.util.Currency;
    import java.util.List;
    import java.util.Objects;
    import java.util.UUID;
    
    public class Main {
    
        // --- VALUE OBJECT ----------------------------------------------------
        // No identity of its own; two Moneys with the same amount+currency ARE equal.
        // Java records give you immutability and value-equality for free.
        public record Money(BigDecimal amount, Currency currency) {
            public Money {                                  // compact constructor = validation
                Objects.requireNonNull(amount, "amount");
                Objects.requireNonNull(currency, "currency");
                if (amount.signum() < 0) throw new IllegalArgumentException("amount < 0");
            }
            public static Money of(String amount, String code) {
                return new Money(new BigDecimal(amount), Currency.getInstance(code));
            }
            public Money add(Money other) {
                if (!currency.equals(other.currency))       // a business rule, IN the domain
                    throw new IllegalArgumentException("Cannot add " + currency + " to " + other.currency);
                return new Money(amount.add(other.amount), currency);  // returns a NEW Money
            }
            @Override public String toString() { return currency.getCurrencyCode() + " " + amount; }
        }
    
        // --- ENTITY ----------------------------------------------------------
        // Has a stable IDENTITY (OrderId). Two orders with the same id are the same order,
        // even if their contents differ.
        public record OrderId(String value) {
            public static OrderId generate() { return new OrderId("ORD-" + UUID.randomUUID()); }
        }
    
        public enum OrderStatus { PENDING, COMPLETED, CANCELLED }
        public record OrderLine(String sku, Money subtotal) {}
    
        // A RICH entity: identity + DATA + the BEHAVIOUR that protects its own rules.
        // This is the opposite of an "anaemic" model (data with no behaviour).
        public static final class Order {
            private final OrderId id;
            private final List<OrderLine> lines;
            private OrderStatus status = OrderStatus.PENDING;
    
            public Order(OrderId id, List<OrderLine> lines) {
                if (lines == null || lines.isEmpty())       // invariant enforced at the boundary
                    throw new IllegalArgumentException("Order must have at least one item");
                this.id = id;
                this.lines = List.copyOf(lines);            // defensive copy -> immutable inside
            }
    
            public Money total() {                          // logic lives WITH the data
                return lines.stream().map(OrderLine::subtotal).reduce(Money::add).orElseThrow();
            }
    
            public void complete() {                        // a state transition, validated
                if (status != OrderStatus.PENDING)
                    throw new IllegalStateException("Can only complete PENDING orders, was " + status);
                status = OrderStatus.COMPLETED;
            }
    
            public void cancel() {
                if (status == OrderStatus.COMPLETED)
                    throw new IllegalStateException("Cannot cancel a completed order");
                status = OrderStatus.CANCELLED;
            }
    
            public OrderId id() { return id; }
            public OrderStatus status() { return status; }
        }
    
        public static void main(String[] args) {
            // Money is immutable and validates itself.
            Money total = Money.of("79.99", "USD").add(Money.of("29.99", "USD"));
            System.out.println("Two items total: " + total);                       // USD 109.98
            System.out.println("Equal by value?  " + total.equals(Money.of("109.98", "USD")));  // true
    
            // The Order ENFORCES its own rules — no service needed to keep it valid.
            Order order = new Order(new OrderId("ORD-001"), List.of(
                    new OrderLine("KBD-1", Money.of("79.99", "USD")),
                    new OrderLine("MSE-1", Money.of("29.99", "USD"))));
            System.out.println("Order " + order.id().value() + " = " + order.total() + " (" + order.status() + ")");
            order.complete();
            System.out.println("After complete -> " + order.status());             // COMPLETED
    
            // Try to break a rule: the domain refuses.
            try { order.cancel(); }
            catch (IllegalStateException e) { System.out.println("Refused: " + e.getMessage()); }
            try { new Order(OrderId.generate(), List.of()); }
            catch (IllegalArgumentException e) { System.out.println("Refused: " + e.getMessage()); }
        }
    }
    Output
    Two items total: USD 109.98
    Equal by value?  true
    Order ORD-001 = USD 109.98 (PENDING)
    After complete -> COMPLETED
    Refused: Cannot cancel a completed order
    Refused: Order must have at least one item
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    3️⃣ Ports, Use Cases & Dependency Inversion

    The application layer answers "what can this system do?" Each capability is a use case (also called an interactor) — one class that orchestrates a single workflow, like "place an order." It builds domain objects, calls their methods, and coordinates the steps. It contains orchestration, not business rules — those live in the domain.

    A use case needs a database and a payment gateway, but it must not know about them. So it declares ports: plain interfaces it owns. An input port is the use case interface the outside world may call (PlaceOrderUseCase). An output port is a capability the use case needs from the world (OrderRepository, PaymentPort). The use case depends only on these interfaces.

    This is the Dependency Inversion Principle: instead of the high-level use case depending on a low-level JpaOrderRepository, both depend on the OrderRepository abstraction. The use case receives a concrete implementation through its constructor (constructor injection) — it never writes new JpaOrderRepository(). The actual wiring happens once, at startup, in a configuration class.

    Worked Example — Application Layer (Ports + Use Case)
    // APPLICATION layer. It depends ONLY on the domain (never on Spring, JDBC, HTTP).
    // It defines INPUT ports (what callers may do) and OUTPUT ports (what it needs
    // from the outside world), then wires them together in a use case.
    //
    // === Output ports — interfaces the use case CALLS ===================
    package com.myapp.application.port.out;
    
    import com.myapp.domain.model.Order;
    import com.myapp.domain.model.OrderId;
    import java.util.Optional;
    
    // "I need somewhere to load and store orders" — but NOT a database. An interface.
    public interface OrderRepository {
        Optional<Order> findById(OrderId id);
        void save(Order order);
    }
    
    // ---------------------------------------------------------------------
    package com.myapp.application.port.out;
    
    import com.myapp.domain.model.Money;
    
    // "I need to take a payment" — not Stripe, not PayPal. Just the capability.
    public interface PaymentPort {
        void charge(Money amount, String paymentMethod);
    }
    
    // === Input port — the use case the OUTSIDE world is allowed to call ==
    package com.myapp.application.port.in;
    
    import com.myapp.domain.model.OrderId;
    import java.util.List;
    
    public interface PlaceOrderUseCase {
        OrderId place(List<String> skus);
    }
    
    // === Use case implementation — orchestration only ====================
    package com.myapp.application.usecase;
    
    import com.myapp.application.port.in.PlaceOrderUseCase;
    import com.myapp.application.port.out.OrderRepository;
    import com.myapp.application.port.out.PaymentPort;
    import com.myapp.domain.model.Order;
    import com.myapp.domain.model.OrderId;
    import java.util.List;
    
    public class PlaceOrderService implements PlaceOrderUseCase {
        private final OrderRepository orders;     // depends on the PORT (interface)...
        private final PaymentPort payment;        // ...never on a concrete adapter.
    
        // DEPENDENCY INVERSION: the high-level use case and the low-level adapter
        // BOTH depend on these interfaces. The arrow points inward, toward the app.
        public PlaceOrderService(OrderRepository orders, PaymentPort payment) {
            this.orders = orders;
            this.payment = payment;
        }
    
        @Override
        public OrderId place(List<String> skus) {
            // 1) Build the domain object (it validates its own invariants).
            Order order = buildOrder(skus);
            // 2) Charge through the payment PORT — real impl decided at startup.
            payment.charge(order.total(), "card");
            // 3) Persist through the repository PORT.
            orders.save(order);
            // 4) Hand back just the identity. No JPA entity, no HTTP type leaks out.
            return order.id();
        }
    
        private Order buildOrder(List<String> skus) { /* map skus -> OrderLines */ return null; }
    }
    These are several files (one per package declaration) from the application layer — not a single runnable main. Drop them into a Maven/Gradle project with the matching folders; the use case compiles against any class that implements its ports.

    🎯 Your Turn #1 — Point the Arrows Inward

    The dependency rule is about which package each file is allowed to import. Fill in the two ___ blanks so the domain only imports the domain, and the use case depends on the port interface — never the concrete adapter.
    🎯 YOUR TURN — Fill in the blanks
    // 🎯 YOUR TURN — make the dependency arrows point the right way.
    //
    // The DEPENDENCY RULE: source code dependencies point INWARD only.
    //   presentation -> application -> domain   (allowed)
    //   domain -> infra                         (FORBIDDEN)
    //
    // Fill in the blanks marked with ___ so the imports obey the rule.
    
    // File: domain/model/Order.java  (the innermost layer)
    package com.myapp.domain.model;
    
    // 1) The domain may ONLY import other domain code. Which package is safe here?
    import com.myapp.___.model.Money;   // 👉 domain (NEVER infra or application)
    
    public final class Order { /* ... pure business logic ... */ }
    
    
    // File: application/usecase/PlaceOrderService.java
    package com.myapp.application.usecase;
    
    // 2) The use case depends on the PORT, not the concrete adapter.
    //    Import the interface, not JpaOrderRepository.
    import com.myapp.application.port.out.___;   // 👉 OrderRepository (the interface)
    
    public class PlaceOrderService { /* depends on the interface only */ }
    
    // ✅ Expected: domain imports only "domain"; the service imports the PORT
    //    interface "OrderRepository", never the concrete "JpaOrderRepository".
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    4️⃣ Adapters & Enforcing the Rule

    An adapter is an outer-layer class that implements a port. JpaOrderRepository implements OrderRepository; StripePaymentAdapter implements PaymentPort. This is the only place framework details — @Repository, JPA mapping, the Stripe SDK — are allowed to appear. At its edges, an adapter maps between the outside shape (a database row, a JSON body) and the domain object, so neither side leaks into the other.

    The dependency rule is easy to state and easy to break by accident. The fix is to make it a test. ArchUnit lets you assert architecture in plain JUnit: "no class in ..domain.. may depend on ..infra..." Add it to your suite and mvn test goes red the moment someone imports the wrong package — a guardrail, not a guideline on a wiki.

    Worked Example — Infrastructure Adapters + ArchUnit Guard
    // INFRASTRUCTURE layer. Adapters live on the OUTSIDE. They IMPLEMENT the ports
    // the use case declared, so you can swap them without touching domain or app.
    // This is the only layer allowed to mention Spring, JPA, Stripe, Kafka...
    package com.myapp.infra.persistence;
    
    import com.myapp.application.port.out.OrderRepository;   // implements the inner port
    import com.myapp.domain.model.Order;
    import com.myapp.domain.model.OrderId;
    import org.springframework.stereotype.Repository;
    import java.util.Optional;
    
    @Repository                                               // framework annotation: fine HERE
    public class JpaOrderRepository implements OrderRepository {
        private final SpringDataOrderRepo db;
    
        public JpaOrderRepository(SpringDataOrderRepo db) { this.db = db; }
    
        @Override public Optional<Order> findById(OrderId id) {
            // Map the PERSISTENCE row -> the DOMAIN entity at the boundary.
            return db.findById(id.value()).map(OrderMapper::toDomain);
        }
    
        @Override public void save(Order order) {
            db.save(OrderMapper.toEntity(order));            // domain -> row, also at the boundary
        }
    }
    
    // --- a second adapter for the payment port ---------------------------
    package com.myapp.infra.payment;
    
    import com.myapp.application.port.out.PaymentPort;
    import com.myapp.domain.model.Money;
    import org.springframework.stereotype.Component;
    
    @Component
    public class StripePaymentAdapter implements PaymentPort {
        @Override public void charge(Money amount, String method) {
            // The real Stripe SDK call lives here. The domain neither knows nor cares.
        }
    }
    
    // === ArchUnit — turn the dependency rule into a FAILING test =========
    package com.myapp;
    
    import com.tngtech.archunit.junit.AnalyzeClasses;
    import com.tngtech.archunit.junit.ArchTest;
    import com.tngtech.archunit.lang.ArchRule;
    import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
    
    @AnalyzeClasses(packages = "com.myapp")
    class ArchitectureTest {
        // The domain must not reach outward to infra or presentation.
        @ArchTest static final ArchRule domain_is_pure =
            noClasses().that().resideInAPackage("..domain..")
                .should().dependOnClassesThat()
                .resideInAnyPackage("..infra..", "..presentation..");
    
        // The application layer must not depend on Spring.
        @ArchTest static final ArchRule application_is_framework_free =
            noClasses().that().resideInAPackage("..application..")
                .should().dependOnClassesThat()
                .resideInAPackage("org.springframework..");
    }
    // If anyone imports infra from the domain, "mvn test" goes red. Guard, not guideline.
    Infra adapters need a Spring Boot project; the ArchUnit test runs in your normal JUnit 5 suite (add com.tngtech.archunit:archunit-junit5), so mvn test fails the build if the dependency rule is ever broken.

    🎯 Your Turn #2 — Add a Port & an Adapter

    Your app needs to send "order shipped" emails without the use case knowing anything about SMTP. Declare an output port and an adapter that fulfils it. Fill in the keywords in the two ___ blanks — what makes a port, and how an adapter satisfies it.
    🎯 YOUR TURN — Fill in the blanks
    // 🎯 YOUR TURN — add an OUTPUT port and an ADAPTER for sending email.
    // The use case must not know about SMTP — only the capability "notify".
    
    // File: application/port/out/NotificationPort.java
    package com.myapp.application.port.out;
    
    // 1) Declare the CAPABILITY the use case needs, as an interface.
    public ___ NotificationPort {            // 👉 a port is an interface
        void orderShipped(String email, String orderId);
    }
    
    // File: infra/email/SmtpNotificationAdapter.java
    package com.myapp.infra.email;
    
    import com.myapp.application.port.out.NotificationPort;
    
    // 2) The adapter IMPLEMENTS the port. This is where SMTP details live.
    public class SmtpNotificationAdapter ___ NotificationPort {   // 👉 implements the port
        @Override
        public void orderShipped(String email, String orderId) {
            // real javax.mail / SMTP call goes here — invisible to the application
            System.out.println("Email to " + email + ": order " + orderId + " shipped");
        }
    }
    
    // ✅ Expected: NotificationPort is an "interface"; the adapter uses
    //    "implements NotificationPort". Swap SMTP for a fake in tests with no
    //    change to the use case.
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    5️⃣ A Package Structure That Makes the Rule Obvious

    Pick a package layout where the layers are visible at a glance, so a wrong dependency stands out in a code review (and ArchUnit catches the rest). Group by layer first, then by feature inside. Read it inside-out: domain has no dependencies, application depends only on the domain, and the outer folders hold the adapters.

    Recommended Package Structure
    # Recommended package layout. Read it INSIDE-OUT: domain has zero
    # dependencies, then application, then the outer adapter layers.
    
    src/main/java/com/myapp/
    ├── domain/                # The core. Knows nothing about the outside world.
    │   ├── model/             #   Entities + Value Objects (Order, Money, OrderId)
    │   ├── event/             #   Domain events (OrderPlaced, OrderCancelled)
    │   └── exception/         #   Domain exceptions (OrderNotFoundException)
    ├── application/           # Use cases. Depends on domain ONLY.
    │   ├── port/
    │   │   ├── in/            #   Input ports  — use-case interfaces (driving side)
    │   │   └── out/           #   Output ports — repo/gateway interfaces (driven side)
    │   └── usecase/           #   Use-case implementations (orchestration only)
    ├── infra/                 # Adapters that IMPLEMENT the out ports.
    │   ├── persistence/       #   JPA repositories + mappers
    │   ├── payment/           #   Stripe / PayPal adapters
    │   └── messaging/         #   Kafka producers / consumers
    └── presentation/          # Adapters that CALL the in ports.
        ├── rest/              #   @RestController, request/response DTOs
        └── config/            #   Spring @Configuration — wires ports to adapters
    This is a folder layout, not runnable code — create these directories under src/main/java in your Maven or Gradle project.

    Common Errors (and the Fix)

    • Domain depending on a framework — your Order imports org.springframework or a JPA @Entity, so the domain can no longer be tested or reused without that framework. Fix: keep the domain pure Java; let an outer adapter map to/from persistence types at the boundary.
    • Anaemic domain model — entities are bags of getters/setters and every rule lives in a fat service. Fix: move the behaviour onto the object that owns the data — order.complete(), money.add() — so invariants are enforced where the state lives.
    • Leaking infrastructure into the domain — a JPA Order entity (with @Column, lazy proxies, a no-arg constructor) is passed around as if it were the domain object. Fix: use a separate persistence entity and a mapper; the domain object stays clean.
    • Circular dependencies between layers — the use case calls the adapter and the adapter calls back into the use case, so neither compiles in isolation. Fix: depend on a port interface in one direction only; introduce a domain event if the outer layer needs to react.
    • Newing dependencies inside a use casenew JpaOrderRepository() inside the service hard-wires it to one database and breaks dependency inversion. Fix: take the OrderRepository port through the constructor and wire the concrete adapter once, in configuration.

    Pro Tips

    • 💡 Records for value objectsrecord Money(BigDecimal amount, Currency currency) {} gives you immutability and value-equality for free, exactly what a value object needs.
    • 💡 Enforce architecture with ArchUnit — a single test that says "domain must not depend on infra" turns the dependency rule into a build failure, not a hope.
    • 💡 Prefer domain events over direct coupling — instead of the use case calling an EmailService directly, publish an OrderPlaced event and let a handler react. The outer layer plugs in without the inner layer knowing.
    • 💡 Start simple, grow deliberately — begin with a rich domain plus repository ports; add use cases, more value objects, and events only as the business rules actually multiply.

    📋 Quick Reference

    ConceptIn JavaPurpose
    Dependency ruleimports point inward onlyDomain stays framework-free
    Entityclass Order (has OrderId)Identity + behaviour
    Value objectrecord Money(amount, currency)Immutable, equal by value
    Input portinterface PlaceOrderUseCaseWhat the outside may call
    Output portinterface OrderRepositoryWhat the app needs (WHAT)
    AdapterJpaOrderRepository implementsConcrete implementation (HOW)
    Use case / interactorclass PlaceOrderServiceOrchestrates one workflow
    Dependency inversionconstructor takes the portBoth depend on the interface
    Domain eventrecord OrderPlaced(OrderId)Decoupled cross-boundary signal

    Frequently Asked Questions

    What exactly is the dependency rule in Clean Architecture?

    Source-code dependencies may only point inward, toward higher-level policy. Presentation depends on application, application depends on domain, and the domain depends on nothing. An inner layer must never import an outer one, so your domain never references Spring, JPA, or HTTP. Tools like ArchUnit can turn this rule into a unit test that fails the build the moment someone breaks it.

    How is Hexagonal Architecture (Ports & Adapters) related to Clean Architecture?

    They are the same idea drawn differently. Clean Architecture pictures concentric circles; Hexagonal pictures a core with ports on its edges. In both, a 'port' is an interface your application owns (OrderRepository, PaymentPort) and an 'adapter' is an outer implementation of it (JpaOrderRepository, StripePaymentAdapter). Input ports are the use cases the world may call; output ports are the capabilities the application needs from the world. The point is identical: keep business logic free of framework details.

    What is the difference between an Entity and a Value Object?

    An Entity has a stable identity that outlives its data — two Orders with the same OrderId are the same order even if their lines differ, so you compare them by id. A Value Object has no identity; it is defined entirely by its values, so two Money instances of USD 10.00 are interchangeable and equal. In Java, model entities as classes with an id field and value objects as immutable records that get value-equality for free.

    How do use cases and the dependency inversion principle fit together?

    A use case (interactor) is a class that orchestrates one application workflow — place an order, cancel an order — by calling domain objects and output ports. It needs a database and a payment gateway, but instead of newing them up it declares interfaces (ports) and receives implementations through its constructor. That is dependency inversion: the high-level use case and the low-level adapter both depend on the abstraction, and the concrete wiring happens once, at startup, in a configuration class.

    Isn't all this layering overkill for a small CRUD app?

    Often, yes. Clean Architecture pays off when a domain has real, changing business rules and a long life. For a three-table CRUD app it adds ceremony without much benefit, so a thinner approach is fine. A good middle path is to start with a rich domain plus repository ports, then add use cases, value objects, and domain events only as the rules grow. The cost you must avoid is leaking framework details into the domain — that is hard to undo later regardless of app size.

    Mini-Challenge — A Cancel-Order Use Case

    Now design one yourself, across the layers. The starter below is an outline only: follow the numbered steps, keep every dependency pointing inward, and put the cancellation rule in the Order entity — not the service. Check your design against the expected behaviour.
    🎯 MINI-CHALLENGE — Design it yourself
    // 🎯 MINI-CHALLENGE: design a "cancel order" use case, clean-architecture style.
    // Write it from the outline — no filled-in logic given this time.
    //
    // Layers to touch (point every dependency INWARD):
    // 1. domain/model/Order      — add a cancel() method that throws if already shipped.
    // 2. application/port/out/    — reuse OrderRepository (findById + save).
    // 3. application/port/in/     — define CancelOrderUseCase { void cancel(OrderId id); }
    // 4. application/usecase/     — CancelOrderService implements CancelOrderUseCase:
    //        a) load the Order via the repository PORT (throw if not found),
    //        b) call order.cancel()  (the DOMAIN enforces the rule),
    //        c) save the Order via the repository PORT.
    // 5. infra/                   — keep the existing JpaOrderRepository adapter; no change.
    //
    // Rules to respect:
    //   - The service depends on INTERFACES only (no JpaOrderRepository import).
    //   - No Spring / JPA annotations in domain or application.
    //   - The cancellation RULE lives in Order, not in the service.
    //
    // ✅ Expected behaviour:
    //   cancel(existingPendingId) -> order.status() becomes CANCELLED, saved once.
    //   cancel(shippedId)         -> IllegalStateException from the domain.
    //   cancel(unknownId)         -> OrderNotFoundException from the service.
    
    // your code here
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎉 Lesson Complete!

    You can now structure a Java application the clean way: a pure domain of entities and value objects that own their rules, an application layer of use cases that orchestrate workflows through ports, and outer adapters that implement those ports behind frameworks. The dependency rule keeps every arrow pointing inward, dependency inversion keeps Spring and JPA out of your business logic, and an ArchUnit test keeps it that way for good.

    Next up: the Final Project — build a complete Java application that applies everything you've learned across the course.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service