The pricing code of this series has taken its tax rate as a parameter since Stage 01. Now the rate has to come from a service that another team runs, orders have to be found again by an identifier, and every quote has to be recorded. None of the ten pricing rules changes, yet all three requirements want a place in the routine that applies them.
The architectures named between 2005 and 2012 state one rule in several vocabularies: the code that holds the business rules refers to nothing else, and everything else refers to it. The rule is worth its cost only where the rules and their surroundings change at different speeds or in different hands.
This part takes from Part 6 code that can be moved without changing what it does, and from Part 5 the principle that says which way a dependency should point, and asks where the boundaries of a system go.
From levels to a domain at the centre
Levels, and what sits at the bottom
Arranging software in levels is older than any name in this part. Edsger W. Dijkstra's paper on the THE multiprogramming system, published in Communications of the ACM in 1968, describes an operating system built that way. In his words: "The total system admits a strict hierarchical structure." His vocabulary is levels and hierarchy; layers, as the name of a pattern, came later.
David Garlan and Mary Shaw's An Introduction to Software Architecture, a book chapter of 1993 and a Carnegie Mellon technical report of January 1994, surveys common architectural styles and counts layered systems among them.
Each level uses the one beneath it, so whatever is lowest is what everything else depends on. In an operating system the lowest level allocates the processor. In a business application arranged the same way, with screens over rules over data access, the lowest level is the database, and the rules are written in terms of it.
A model with a vocabulary
Eric Evans's Domain-Driven Design: Tackling Complexity in the Heart of Software was published by Addison-Wesley on 20 August 2003, with a foreword by Martin Fowler and a 2004 copyright date, which is why both years are cited. The publisher lists four parts: Putting the Domain Model to Work, The Building Blocks of a Model-Driven Design, Refactoring Toward Deeper Insight and Strategic Design.
The book did not invent all of its words: entity and repository were in use before it in other senses. What it did was assemble a set of terms and define them against each other. Evans published the set again in 2015 as a freely licensed summary, the Domain-Driven Design Reference. The definitions below are this series' own wording, not his:
| Term | What it names | In the example |
|---|---|---|
| Ubiquitous language | One vocabulary that developers and domain experts both use, the same in speech, in documents and in code | The rule list says the discount is capped; the code has a type called DiscountCap |
| Bounded context | An explicit boundary inside which one model applies and each term has one meaning | One context only, so nothing to show |
| Entity | An object that stays the same thing through changes to its attributes, because it has an identity | Order, with a reservation described below |
| Value object | An object that is nothing but its attributes: no identity, treated as immutable | Money, Percentage, Quantity, Coupon |
| Aggregate | A group of objects changed as one unit and reached only through a root entity that keeps its invariants | Order and its lines |
| Repository | Something that looks like an in-memory collection of the aggregates of one type and hides how they are stored | IOrderRepository |
Ports and adapters, the onion and the dependency rule
Alistair Cockburn published The Hexagonal (Ports & Adapters) Architecture as a technical report dated 4 September 2005. Ports and Adapters is the pattern's name; Hexagonal Architecture is given as the alternative. The report says the six sides mean nothing: the shape leaves room to draw the ports and the adapters. Earlier uses of the idea are often dated to the 1990s; the report does not date them, and neither does this article. Its stated intent:
"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases."
Jeffrey Palermo named the Onion Architecture in a post of 29 July 2008, the first of a series. The domain model is at the centre and the layers are drawn around it. His rule: "The fundamental rule is that all code can depend on layers more central, but code cannot depend on layers further out from the core." The post relies on the Dependency Inversion Principle to make that possible.
Robert C. Martin's post The Clean Architecture, dated 13 August 2012, does not present a new architecture. It presents itself as an attempt to integrate earlier ones, crediting Hexagonal Architecture to Cockburn, Onion Architecture to Palermo, DCI to James Coplien and Trygve Reenskaug and BCE to Ivar Jacobson, beside Martin's own Screaming Architecture. What it adds is a name for the rule they share, the Dependency Rule:
"This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle."
The book of the same name followed on 10 September 2017, with a 2018 copyright date.
| Date | Who | What, in the original terms |
|---|---|---|
| 1968 | Edsger W. Dijkstra | The THE multiprogramming system: a strict hierarchy of levels |
| 1993, 1994 | David Garlan and Mary Shaw | An Introduction to Software Architecture: architectural styles, layered systems among them |
| 1996 | Robert C. Martin | The Dependency Inversion Principle, a column in The C++ Report |
| 20 August 2003 | Eric Evans | Domain-Driven Design: a defined vocabulary for a model of the domain |
| 4 September 2005 | Alistair Cockburn | The Hexagonal (Ports & Adapters) Architecture, HaT Technical Report 2005.02 |
| 29 July 2008 | Jeffrey Palermo | The Onion Architecture : part 1 |
| 13 August 2012 | Robert C. Martin | The Clean Architecture and its Dependency Rule |
| 25 March 2014 | James Lewis and Martin Fowler | Microservices |
| 10 September 2017 | Robert C. Martin | Clean Architecture, the book |
The table is not a line of succession. Levels with the database at the bottom are still the cheapest arrangement for an application that is mostly forms over tables.
One rule under three names
Cockburn's ports, Palermo's layers and Martin's circles differ in what they draw and agree on the direction of the arrows. The rules of the business are in the middle and name nothing outside themselves. Storage, transport, screens and schedulers are outside and name the middle.
That is the Dependency Inversion Principle of Martin's 1996 column, which Part 5 applied to one calculator, applied to a larger unit. There the abstraction was an interface between classes. Here it is an interface between whole namespaces or assemblies, and the detail that decides everything is who owns it. If the interface for storing orders is declared beside the database code, the rules depend on the database's side of the system. If it is declared beside the rules, in their terms, the database code depends on the rules.
Evans's vocabulary answers a different question. The three architectures say where the model is placed and what may refer to it. Domain-driven design says what is in the model and what its words mean. The example takes the two in separate stages.
Stage 09: a model that cannot hold a wrong value
Value objects
Stage 02 of the example passes amounts and rates around as decimal and checks its input in one function, Validate, called first. Stage 09 moves each check into a type that cannot be constructed with a value the rules forbid. A percentage is one of four such types:
public sealed record Percentage
{
private Percentage(decimal fraction) => Fraction = fraction;
public decimal Fraction { get; }
public static Percentage Zero { get; } = new(0m);
public static Percentage Of(decimal percent) => FromFraction(percent / 100m);
public static Percentage FromFraction(decimal fraction) =>
fraction is < 0m or > 1m
? throw new DomainException("A percentage lies between 0 and 100.")
: new Percentage(fraction);
// A percentage of an amount is itself an amount of money, so it is rounded here.
public Money AppliedTo(Money amount) => amount.Times(Fraction).Rounded();
}The constructor is private, so Of and FromFraction are the only ways in, and both refuse a value outside 0 to 100. Rounding is no longer something each caller remembers: applying a percentage to money rounds, because a share of an amount is itself an amount. Quantity refuses anything below one. Coupon.Parse returns nothing for no code, the one known coupon for WELCOME10, and throws for any other text. Money is a readonly record struct with a private constructor. In Stage09DomainTests, Value_objects_are_equal_by_value asserts the equality and Value_objects_cannot_hold_a_forbidden_value the refusals.
OrderLine and Discount are records and behave as value objects as well. Quote is a record too, but it holds a list, and a C# record compares a list member by reference, so two quotes with the same figures are not equal. Nothing in the example compares two.
The aggregate
// The aggregate root. Lines are reached only through the order, and the order is the
// only thing that can be asked for a price, so no caller can price half an order or
// skip the cap.
public sealed class Order
{
private readonly List<OrderLine> _lines;
private Order(List<OrderLine> lines, CustomerTier tier, bool isFirstOrder, Coupon? coupon)
{
_lines = lines;
Tier = tier;
IsFirstOrder = isFirstOrder;
Coupon = coupon;
}
public static Order Place(
IEnumerable<OrderLine> lines, CustomerTier tier, bool isFirstOrder, Coupon? coupon = null)
{
List<OrderLine> placed = [.. lines];
if (placed.Count == 0) throw new DomainException("An order needs at least one line.");
if (!Enum.IsDefined(tier)) throw new DomainException("Unknown customer tier.");
return new Order(placed, tier, isFirstOrder, coupon);
}
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
public CustomerTier Tier { get; }
public bool IsFirstOrder { get; }
public Coupon? Coupon { get; }
public Money Subtotal => _lines.Aggregate(Money.Zero, (sum, line) => sum + line.Total);The region stops inside the class, which continues with the pricing method shown below. What the aggregate protects can be read from it. An order is created only through Place, which copies the lines it is given and rejects an empty list and an undefined tier. The lines are exposed read-only, and Stage09DomainTests.The_lines_of_an_order_cannot_be_changed_from_outside checks that a cast back to a mutable list fails. A line cannot be built with a negative price or a quantity below one. So an Order that exists is a valid order, and the pricing code after it contains no validation at all.
Two reservations belong here. This Order has no identity of its own. By the definition in the table it is the root of an aggregate without yet being an entity; Stage 09 never needs to find an order again, and Stage 10 adds the identity from outside. The second is that Quote has a public constructor, so the aggregate guarantees that every quote it returns respects the cap, not that every Quote in a program came from it.
The same rules, before and after
Here is the top routine of Stage 02:
public static PriceBreakdown Price(OrderData order, decimal taxRate = 0.23m)
{
Validate(order, taxRate);
decimal subtotal = Subtotal(order);
decimal volume = VolumeDiscount(subtotal);
decimal loyalty = LoyaltyDiscount(order.Tier, subtotal);
decimal coupon = CouponDiscount(order.CouponCode, order.IsFirstOrder, subtotal);
decimal discount = CapDiscount(volume + loyalty + coupon, subtotal);
decimal shipping = Shipping(subtotal - discount);
decimal tax = Tax(subtotal - discount + shipping, taxRate);
decimal total = RoundMoney(subtotal - discount + shipping + tax);
return new PriceBreakdown(
subtotal,
[new("volume", volume), new("loyalty", loyalty), new("coupon", coupon)],
discount, shipping, tax, total);
}And the method that continues the Order class in Stage 09:
public Quote Price(DiscountPolicy discounts, Percentage taxRate)
{
IReadOnlyList<Discount> granted = discounts.DiscountsFor(this);
Money requested = granted.Aggregate(Money.Zero, (sum, discount) => sum + discount.Amount);
Money discount = DiscountCap.Apply(Subtotal, requested);
Money afterDiscount = Subtotal - discount;
Money shipping = ShippingRule.For(afterDiscount);
Money tax = taxRate.AppliedTo(afterDiscount + shipping);
Money total = (afterDiscount + shipping + tax).Rounded();
return new Quote(Subtotal, granted, discount, shipping, tax, total);
}What did not change: the order of the steps, the arithmetic, and every point at which an amount is rounded. What changed: the call to Validate is gone, because the types did that work when the order was placed; decimal became Money and Percentage; the three discounts come from DiscountPolicy, a stateless class that the example's comments call a domain service; and the cap and the shipping rule are named types, DiscountCap and ShippingRule, where Stage 02 had private functions with literal numbers in them. StageEquivalenceTests.Every_stage_prices_every_order_in_the_table_identically runs both against the same 22 hand-computed orders, and InvalidInputTests.Every_stage_rejects_invalid_input shows that both refuse the same inputs, Stage 02 with an ArgumentException and Stage 09 with a DomainException.
Inside DiscountPolicy the loyalty rate is still a switch over the tier and the coupon rate is still a conditional. A domain model does not remove the branches; it gives each a named home.
Stage 10: ports, adapters and a use case
Stage 10 leaves the Stage 09 model unchanged and adds one type to it, OrderId, a record struct around a string. Then it answers the three requirements this part opened with, each as an interface that the application declares:
// Ports: what the use case needs from the outside world, stated in the application's
// own terms and in domain types. The application owns these interfaces; adapters
// implement them. That ownership is what makes the dependency point inward.
public interface IOrderRepository
{
Order? Find(OrderId id);
void Save(OrderId id, Order order);
}
public interface ITaxRateProvider
{
Percentage CurrentRate();
}
public interface IQuoteJournal
{
void Record(OrderId id, Quote quote);
}These are the ports, written in domain types: a tax rate is a Percentage, not a number. The order's identity travels beside the order, as the key under which the repository stores it, and is not a member of the aggregate. The code that uses the ports is short:
public sealed class OrderNotFoundException(OrderId id) : Exception($"Order '{id}' was not found.");
// The use case: one thing the system does for its user, from input to result. It
// sequences the work and delegates every decision to the domain and every effect to a
// port. It can be read, and tested, without a database, a network or a clock.
public sealed class PriceOrder(
IOrderRepository orders,
ITaxRateProvider taxRates,
IQuoteJournal journal,
DiscountPolicy discounts)
{
public Quote Execute(OrderId id)
{
Order order = orders.Find(id) ?? throw new OrderNotFoundException(id);
Quote quote = order.Price(discounts, taxRates.CurrentRate());
journal.Record(id, quote);
return quote;
}
}PriceOrder makes no pricing decision. It finds the order, asks it for its price, records the result and returns it. There is no inbound interface: the driving side is the Execute method itself, and the console host, the stage catalogue and the tests all drive it, which is the first half of Cockburn's intent in miniature.
The adapters are in Adapters/InMemoryAdapters.cs: InMemoryOrderRepository keeps orders in a dictionary, InMemoryQuoteJournal in a list, and FixedTaxRateProvider turns a decimal into a Percentage when asked. That conversion stops a bad rate: Stage10Tests.An_invalid_rate_from_an_adapter_is_rejected_by_the_domain gives the adapter 1.5 and gets a DomainException before any price is computed. A second implementation of that port exists only in the tests, and Stage10Tests.The_tax_rate_comes_from_the_port_and_from_nowhere_else prices an order at 6% through it.
Pricing.Host console app, the only project with a container
| registers each adapter against its port
v
Stage10Boundaries.Adapters
InMemoryOrderRepository FixedTaxRateProvider
InMemoryQuoteJournal
| implements the ports | uses domain types
v |
Stage10Boundaries.Application |
IOrderRepository ITaxRateProvider IQuoteJournal
PriceOrder OrderNotFoundException
| uses domain types |
v v
Stage10Boundaries.Domain OrderId
Stage09Domain Order OrderLine Quote Discount
Money Percentage Quantity Coupon
DiscountPolicy DiscountCap ShippingRule
|
v
no other namespace of the solutionThe host composes the graph in a container, as Part 5 explained, and prints the same totals as every earlier stage:
Stage 02 Named functions | capped | subtotal 6000.00 | discount 1200.00 | shipping 0.00 | tax 1104.00 | total 5904.00
Stage 10 Boundaries | capped | subtotal 6000.00 | discount 1200.00 | shipping 0.00 | tax 1104.00 | total 5904.00
Stage 10 through the container | capped | subtotal 6000.00 | discount 1200.00 | shipping 0.00 | tax 1104.00 | total 5904.00What enforces the direction
All eleven stages are compiled into one assembly, Pricing, and every type in Stages 09 and 10 is public. The three rings are namespaces, and the compiler gives a namespace no power to refuse a reference: one using line in the domain would reach an adapter. The direction is therefore held by a test.
[Fact]
public void The_domain_refers_to_nothing_in_the_application_or_the_adapters()
{
Assert.DoesNotContain(ReferencesFrom(Domain09), In(Application, Adapters));
Assert.DoesNotContain(ReferencesFrom(Domain10), In(Application, Adapters));
}
[Fact]
public void The_domain_refers_to_no_other_stage_at_all()
{
string[] own = [Domain09, Domain10];
Assert.DoesNotContain(ReferencesFrom(Domain09, Domain10), type =>
type.Namespace?.StartsWith("Pricing", StringComparison.Ordinal) == true && !own.Contains(type.Namespace));
}
[Fact]
public void The_application_refers_to_nothing_in_the_adapters()
{
Assert.DoesNotContain(ReferencesFrom(Application), In(Adapters));
}
// The positive control: the same reading does find the references that are allowed.
// Without it, a reader that finds nothing would pass the three tests above.
[Fact]
public void The_inward_references_exist_and_are_found()
{
Assert.Contains(ReferencesFrom(Application), In(Domain09));
Assert.Contains(ReferencesFrom(Application), In(Domain10));
Assert.Contains(ReferencesFrom(Adapters), In(Application));
Assert.Contains(ReferencesFrom(Adapters), In(Domain09));
}The second test is stricter than the diagram needs: the domain may not name any other namespace of the solution, including the one that holds the input records every stage shares. That is why the conversion from raw input to domain types lives outside the three rings, in StageCatalogue.ToDomain. The fourth test is a control: a reading that found nothing would pass the first three, so it asserts that the permitted references are found.
The reading needs no extra package. For each type it collects the base type, the interfaces, and the types of fields, properties, parameters, return values and local variables:
private static Predicate<Type> In(params string[] namespaces) =>
type => namespaces.Contains(type.Namespace);
private static HashSet<Type> ReferencesFrom(params string[] namespaces)
{
const BindingFlags Declared = BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
HashSet<Type> found = [];
foreach (Type type in PricingAssembly.GetTypes().Where(type => namespaces.Contains(type.Namespace)))
{
List<Type?> named = [type.BaseType, .. type.GetInterfaces()];
named.AddRange(type.GetFields(Declared).Select(field => field.FieldType));
named.AddRange(type.GetProperties(Declared).Select(property => property.PropertyType));
foreach (MethodBase method in type.GetMethods(Declared).Concat<MethodBase>(type.GetConstructors(Declared)))
{
named.AddRange(method.GetParameters().Select(parameter => parameter.ParameterType));
named.Add((method as MethodInfo)?.ReturnType);
named.AddRange(method.GetMethodBody()?.LocalVariables.Select(local => local.LocalType) ?? []);
}
foreach (Type reference in named.OfType<Type>().SelectMany(Unwrap))
{
if (!namespaces.Contains(reference.Namespace)) found.Add(reference);
}
}
return found;
}
// List<Order>, Order[] and Order? all name Order.
private static IEnumerable<Type> Unwrap(Type type)
{
if (type.HasElementType) return Unwrap(type.GetElementType()!);
if (!type.IsGenericType) return [type];
return type.GetGenericArguments().SelectMany(Unwrap).Append(type.GetGenericTypeDefinition());
}It does not decode method bodies, and the example contains the proof. Order.Price calls DiscountCap.Apply and ShippingRule.For. Both return Money, so the reading records Money and never learns that DiscountCap or ShippingRule exist. A static call from the domain to a method in the adapters that returned a domain type would pass all four tests. So would an object created and discarded in one expression, a cast, or an attribute. The locals are those the compiler chose to emit, and an optimising build may emit fewer.
One boundary in the example is enforced by the compiler. The container package is referenced by the Pricing.Host project and by no other, so nothing in the Pricing library can name it, whatever a test says. That is the stronger tool: put the domain in a project that references nothing, and a wrong reference does not build. We would use projects where a wrong reference would be expensive, and a test like this one where one team owns all the code.
Where a service boundary meets a bounded context
James Lewis and Martin Fowler's article Microservices is dated 25 March 2014. They did not coin the word: a footnote records that the term was discussed at a workshop of software architects near Venice in May 2011, and that the same group settled on the name in May 2012. The article says the style has no precise definition. Its description:
"the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms"
What follows is our judgement, not a position taken from that article. A service boundary is the boundary of Stage 10 made out of a process and a network in place of a namespace. It is the most expensive form, and it can only be as good as the model boundary it encloses. A bounded context is the unit inside which a word has one meaning. A service cut through the middle of a context shares one model across a network. A service that spans two contexts holds two meanings of one word in one codebase. The example has one context and one model, so it cannot show where a second would begin.
What the structure costs
Counted from the files of the example, for the same ten rules and the same output:
| Measure | Stage 02 | Stages 09 and 10 together |
|---|---|---|
| Source files | 1 | 6 |
| Types declared | 1 | 22 |
| Lines, as written | 92 | 332 |
| Lines of code, without comments and blank lines | 69 | 208 |
Stage 02 uses the shared input and output records directly. Stages 09 and 10 also need a mapping to and from them, in StageCatalogue.cs, and a 92-line architecture test. Neither is in the table.
Three times the code buys nothing a customer can see: the host output above is identical. For the people who change the code it buys three things. A rule has a name and one place. A value that the rules forbid cannot be constructed. And the tax-rate source, the store and the journal can each be replaced without opening the file that holds the rules.
In our judgement that is repaid under three conditions. The rules outlive their surroundings, so that a database or a framework will be replaced under them. Several teams change the same system, and a boundary lets one change its side without reading the other's. Or the rules change on a different timetable from delivery.
The pricing example meets none of them: ten rules, one author, no database. For a real system of this size we would stop at Stage 03 or Stage 04 and add a port on the day the tax rate first had to come from somewhere else. Stage 10 exists to show the shape at a size that fits in an article.
Two failures are common enough to name, again as our judgement. The first is the vocabulary without the model: classes called entity, aggregate and repository whose rules still sit in service classes around them. That costs what Stage 09 costs and prevents no invalid state. The second is the building blocks without the questions of language and context that Evans's book also asks. A team can write flawless value objects around one Order class that means four things to four departments.
Most of Stages 09 and 10 is records, interfaces, pattern matching and a container, with few of the 23 patterns of Part 4 visible by name. Part 8 asks what modern C# and .NET still need of the 1994 catalogue.
What to take from this
- Decide which code holds the rules of the business, and make it the code that refers to nothing else. Declare the interfaces it needs beside it, in its own types.
- Move a validation into a type when the same check would otherwise be repeated or forgotten. A
Percentagethat cannot be 150 removes a class of test. - Write the direction of dependencies down as a test or as project references, and know what your check cannot see. A reflection test without a positive control proves nothing.
- Count the types before adopting the structure. Where the rules are few and have one author, named functions or small modules are the better design.
- Draw a service boundary only where a model boundary already is, and find that by asking what each word means to the people who use it.
Run the code
The example is one download, software-foundations-example.zip: unpack it and run the commands in its foundations folder. It was built and its tests were run with the .NET 10 SDK on 13 November 2026, and it needs that SDK. .NET 10 is a long-term support release; .NET 11 was released on 10 November 2026 and the example does not need it.
dotnet testruns the tests.dotnet run --project src/Pricing.Hostprints the output shown above.
The tests that cover what this article showed:
StageEquivalenceTests.Every_stage_prices_every_order_in_the_table_identically: Stages 02, 09 and 10 against the 22 orders ofOrderCases.cs.InvalidInputTests.Every_stage_rejects_invalid_input: Stages 09 and 10 reject withDomainException.Stage09DomainTestsandStage10Tests, both inStage10Tests.cs: the value objects, the cap, the read-only lines, and the use case with the in-memory adapters and with a stub for the tax-rate port.ArchitectureTests: the three rules of direction and the positive control.CompositionTests.The_use_case_composed_by_the_container_prices_the_table: the use case resolved from the host's container.HostOutputTests.The_host_prints_exactly_the_committed_expected_output: the output lines quoted above.
./counterproof.sh changes one rule at a time in a copy of the solution and requires the tests to fail. Three of its thirteen changes fall in the Stage 09 model: the Gold rate, the cap and the rounding mode.
References
- Edsger W. Dijkstra, "The structure of the 'THE'-multiprogramming system", EWD196, published in Communications of the ACM 11(5), 1968.
- David Garlan and Mary Shaw, "An Introduction to Software Architecture", CMU-CS-94-166, Carnegie Mellon University, January 1994.
- Robert C. Martin, "The Dependency Inversion Principle", The C++ Report, 1996.
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison-Wesley, 2003.
- Eric Evans, Domain-Driven Design Reference: Definitions and Pattern Summaries, Domain Language, Inc., 2015.
- Alistair Cockburn, "The Hexagonal (Ports & Adapters) Architecture", HaT Technical Report 2005.02, 4 September 2005.
- Jeffrey Palermo, "The Onion Architecture : part 1", 29 July 2008.
- Robert C. Martin, "The Clean Architecture", 13 August 2012.
- Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design, Robert C. Martin Series, Pearson, 2017.
- James Lewis and Martin Fowler, "Microservices", martinfowler.com, 25 March 2014.
The Foundations of Software Engineering
From the origins of programming to modern architecture. 8 parts, read in order.
- 01Before Design Patterns: How Software Engineering Learned to Manage Complexity
From Lovelace's Notes of 1843 to Brooks's essay of 1986, each advance in programming removed a difficulty of the tools and left the difficulty of the problem. One pricing routine, written three ways with identical output, shows what structure and names buy and what they do not.
- 02Structured Programming, Modularity and Information Hiding
Structured control flow, procedures, modules, information hiding, coupling, cohesion and separation of concerns are separate ideas from separate authors. The same pricing rules, cut as functions and then as modules, show what Parnas's criterion localises and what it does not.
- 03Object-Oriented Programming: Origins, Promises and Trade-offs
Object orientation is several mechanisms with separate origins: Simula's subclasses, abstract data types, Smalltalk's messaging. One pricing example shows what encapsulation and an interface buy, what they cost, and when functions over plain data stay the better design.
- 04The Gang of Four: 23 Design Patterns and the Problems They Solve
The 23 patterns of the 1994 book, each as a problem, a shape, a cost and its modern C# form, in one pricing example. Most are a single move, an interface placed where something varies, and the example needed only one of them.
- 05SOLID, Coupling, Cohesion and Dependency Inversion
Coupling and cohesion are the criteria; the five SOLID principles are later heuristics with separate origins in Meyer, Liskov and Martin. One pricing calculator shows what each buys and costs, and why dependency inversion, dependency injection and a container are three different things.
- 06Refactoring: Improving Design Without Changing Behaviour
Refactoring changes structure against a recorded definition of behaviour, in steps small enough to check. One legacy pricing method is pinned by characterisation tests, reshaped in seven steps, and the tests themselves are proved able to fail.
- 07From Design Patterns to Domain-Driven Design and Modern Architecture
You are here.
- 08Do We Still Need Gang of Four Patterns in Modern C# and .NET?
All 23 patterns set against C# 14 and .NET 10: eleven have a shorter form in the example, built from a language or platform feature, and twelve do not. The problems remain in every case, and each modern form has a failure of its own.
Support independent engineering knowledge
Knowledge, Analysis, the Field Guide and the tools are free to read and use, and they stay that way. If this article saved you time, you can support the research and writing behind it. Support is voluntary and unlocks nothing.
Buy me a coffee (opens in a new tab)João Lívio's page on buymeacoffee.com