Frameworks and Patterns: Building in a City

Revised: 2026-09-22T10:31:17Z
design-patternssoftware-designjavaarchitecture

Frameworks hand you the city layout, patterns hand you the blueprints inside it. Where the line between the two actually falls, and why adopting either never makes a trade-off disappear.

"Why bother with design patterns when the framework already does the work?"

A checkout service built on Spring shows where the question comes from. The framework instantiates the service, injects its collaborators, and wraps each call in a transaction. None of that decides where the shipping rules go. Do they belong in one long method? Does each carrier get its own implementation? What happens when a second checkout flow needs the same rules?

Those are low level design questions, and frameworks do not tell anything about it, but what they tell is what your project shall look like in grand scale.

you can try to use an analogy here, framework gives you an city layout and you decide what each building should look like.

Patterns on the other hand give you layouts of each building in a city and how you shall design the interior, e.g. where the TV should be located is up to you,

the thing both do not touch is private implementation, in this example it is the placement of the TV

The Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) describe each pattern through its intent, the problem it addresses, the objects involved, and the consequences of applying it. The last of these is the one most often skipped. Introducing a pattern redistributes the trade-offs in a design. It does not remove them.

Their three categories remain useful shorthand. Creational patterns concern object creation, structural patterns concern the composition of objects and classes, and behavioral patterns concern how responsibility is divided among collaborating objects. The checkout problem sits in the third group.

Shipping rules as a starting point

Standard and express delivery price a shipment differently, but both take the same shipment details and return a price. The caller needs to choose an algorithm without depending on how it is implemented.

That is the Strategy pattern:

import java.math.BigDecimal;

record Shipment(int weightKg) {}

interface ShippingStrategy {
    BigDecimal calculate(Shipment shipment);
}

final class StandardShipping implements ShippingStrategy {
    @Override
    public BigDecimal calculate(Shipment shipment) {
        return new BigDecimal("5.00")
                .add(BigDecimal.valueOf(shipment.weightKg()));
    }
}

final class ExpressShipping implements ShippingStrategy {
    @Override
    public BigDecimal calculate(Shipment shipment) {
        return new BigDecimal("12.00")
                .add(BigDecimal.valueOf(shipment.weightKg())
                        .multiply(new BigDecimal("2.00")));
    }
}

final class ShippingService {
    private final ShippingStrategy strategy;

    ShippingService(ShippingStrategy strategy) {
        this.strategy = strategy;
    }

    BigDecimal quote(Shipment shipment) {
        return strategy.calculate(shipment);
    }
}

ShippingService is the context: it holds a reference to a strategy and delegates the calculation to it. StandardShipping and ExpressShipping are the concrete strategies.

Wiring the objects together needs nothing beyond ordinary Java:

var service = new ShippingService(new ExpressShipping());
var price = service.quote(new Shipment(3));

Spring could supply that constructor argument instead. The collaboration is identical either way: the service asks its strategy for a price. A pattern describes how objects relate to one another, regardless of how they are instantiated.

Introducing Strategy adds an interface, two classes, and a selection point that some caller or configuration must own. When both calculations are a single line and unlikely to change, a conditional is easier to read and maintain. Strategy earns its overhead when the algorithms change on separate schedules, carry their own dependencies, or have to be reused in more than one flow.

What the framework handles

A framework provides running code and a structure for extending it. In a web application it receives a request, finds the appropriate handler, invokes application code, and turns the result into a response. Application code fills in the extension points the framework defines.

This arrangement is inversion of control. Control over when application code executes rests with the framework rather than with the caller. A library normally leaves that sequencing to the caller, though a single package can offer both styles.

None of this requires a web stack. Consider the beginning of an import framework, in which every job parses lines and persists the resulting objects:

import java.util.List;

abstract class ImportJob<T> {
    public final void run(List<String> lines) {
        beforeImport();

        for (String line : lines) {
            T item = parse(line);
            save(item);
        }

        afterImport();
    }

    protected void beforeImport() {}
    protected abstract T parse(String line);
    protected abstract void save(T item);
    protected void afterImport() {}
}

The run method fixes the order of operations. Subclasses supply parse and save, and may override the optional hooks on either side of the loop. This is the Template Method pattern: the skeleton of an algorithm lives in the base class, and subclasses implement selected steps.

A customer import built on that structure:

record Customer(String name) {}

interface CustomerRepository {
    void save(Customer customer);
}

final class CustomerImport extends ImportJob<Customer> {
    private final CustomerRepository repository;

    CustomerImport(CustomerRepository repository) {
        this.repository = repository;
    }

    @Override
    protected Customer parse(String line) {
        return new Customer(line.strip());
    }

    @Override
    protected void save(Customer customer) {
        repository.save(customer);
    }
}

Once a caller invokes run, the base class drives the process. CustomerImport supplies the details but does not decide whether parsing precedes saving. That ordering is already fixed in the reused code.

The example is deliberately small. A usable import framework would have to settle harder questions. Does a malformed line abort the job? Are earlier writes rolled back? Does afterImport run when saving fails? As written it does not, which rules it out as a cleanup hook.

The pattern name describes the arrangement of methods and responsibilities. Failure behavior is a separate matter, settled by reading the framework's contract.

The trade-off does not go away

A framework contains patterns of its own. Observer may account for its notification mechanism, Adapter for the way it reaches another API. Recognizing them makes unfamiliar framework code easier to navigate, though it leaves the operational questions open. Are notifications synchronous? Does an integration retry a failed call?

The two examples above also differ in a way worth raising in code review. Strategy relies on composition: ShippingService receives an object and delegates work to it. Template Method relies on inheritance: CustomerImport extends a base class and inherits its algorithm. Where the import steps need to vary independently, inheritance becomes awkward, and separate parser and writer collaborators would serve better.

Adopting a framework is the longer commitment. Useful infrastructure arrives bundled with upgrades, configuration, lifecycle rules, and extension points, some of which will not fit the next requirement. Replacing a framework reaches across most of an application. A pattern confined to a handful of classes is usually easier to revise, though it can spread as well.

For the checkout service, I would keep the pricing decision explicit. Where shipping algorithms vary independently, Strategy gives that variation a single home. Spring can then handle the surrounding work of constructing the service and exposing it through a request handler.

The question worth asking at the next change is whether it belongs to the business design or to a framework extension point. Answering that well matters more than counting the patterns a project uses.