Unit 2 · Software engineering · Lesson 3 of 15
Data modeling, ownership and dependency boundaries
Step 1: Learn the ideas
- Entities, value objects and identity
- Representation invariants and abstraction functions
- Making illegal states unrepresentable
- Information hiding and decomposition by likely change
- Cohesion, coupling and separation of concerns
- Data ownership and single writers
- Dependency direction, layering, ports and adapters
- Bounded contexts and translation between models
- Shared databases versus APIs and events
- Relational versus document modeling by access pattern
Why it matters
Code is read and changed far more often than it is written, so the main cost of a design is the cost of the next change. A model with clear owners and enforced invariants confines a change to one place; a model where three modules write the same table, or where business rules live in HTTP handlers, turns every change into an investigation. Object-oriented and low-level design rounds ask you to model a parking garage, a rate limiter or a document editor precisely to see how you choose types and boundaries, and senior interviews return to the same theme at service scale: who owns this data, and what happens when it has to change?
Core ideas
Types carry invariants. A good type makes its valid states explicit and keeps invalid ones unrepresentable. MIT's software construction readings name the two halves: a representation invariant says which internal states are legal, and an abstraction function says what a legal state means. Establish the invariant in the constructor, preserve it in every operation, and never hand out references to mutable internals; that leak, called rep exposure, lets callers break the invariant from outside. Prefer immutable value objects for concepts defined by their value (money, a date range, an email address) and entities with stable identity for things that change over time (an account, an order).
Decompose by what changes. Parnas's 1972 paper argues that each module should hide a design decision likely to change, such as how data is represented or which algorithm is used, rather than mirror a step of a processing flowchart. The same reasoning applies today to vendors, model providers and other external services. A boundary is good if a plausible change touches one module. High cohesion (the parts of a module change together) and low coupling (modules change independently) are the observable results.
Ownership. Every piece of data should have one writer that enforces its invariants. Other components read through that owner's interface, through events it publishes, or through a read replica, and accept the staleness that implies. A database written by several services feels convenient until the schema must change and nobody knows who depends on which column. Inside a monolith the same rule applies to modules and tables.
Dependency direction. Domain logic should not import the web framework, the ORM or a vendor SDK. Put those behind narrow interfaces (ports) implemented by adapters at the edge; the core then runs and is tested with in-memory fakes, and infrastructure can change without touching business rules. Dependencies should point from volatile code toward stable abstractions, and a cycle between packages usually means a boundary is in the wrong place.
Bounded contexts. Large domains rarely share one consistent model: "contact" can mean different things to sales, billing and compliance. Domain-driven design calls each internally consistent region a bounded context and makes translation between contexts explicit instead of forcing one universal schema.
A small illustration: in a multi-tenant outreach tool, a Campaign type could own its status transitions (draft, scheduled, sending, finished) and expose schedule(at) and cancel() instead of a settable status field, so no caller can move a finished campaign back to draft. Messages reference the campaign by ID and are written only by the sending component, which owns delivery state.
Common misconceptions
- "Good modeling means deep inheritance hierarchies." Maintainable designs mostly use composition and small interfaces; inheritance couples a subclass to its parent's implementation.
- "Getters and setters provide encapsulation." A public setter for every field exposes the representation; encapsulation means offering operations that preserve invariants.
- "Microservices create good boundaries." A network boundary in the wrong place produces a distributed monolith with extra failure modes; get module boundaries right first.
- "Always normalize" or "always denormalize." Normalization protects invariants at write time and denormalization serves read paths; decide per access pattern and name the owner of every copy.
- "Never duplicate code." Two fragments that look alike but change for different reasons should stay separate; merging them couples unrelated changes.
How to approach it
Read the MIT 6.102 readings on abstract data types and on abstraction functions and rep invariants, then Parnas's short paper. Practice by modeling three domains on paper in 30 minutes each: write the types, their invariants, the operations that change state and the owner of each table. In interviews, start from use cases and invariants rather than class diagrams, say which component owns which data, and show how the design absorbs a specific change ("adding a second sending channel only adds an adapter"). Graduate answers should be precise about types and invariants; mid-level answers should separate I/O from logic; senior answers should argue ownership and boundaries across teams and plan how the model will evolve.
Suggested order
- 1Read MIT 6.102 “06: Abstract Data Types” and “07: Abstraction Functions & Rep Invariants”; for one type you have written, state its rep invariant and check that every operation preserves it.
- 2Read Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”, and list the design decision each module of a project you know is hiding.
- 3Read DDIA Ch. 2 (Data Models and Query Languages) for relational versus document tradeoffs and how access patterns shape the model.
- 4Read Fowler’s BoundedContext entry and Cockburn’s Hexagonal Architecture; redraw a project you know with ports for the database, external APIs and the clock.
- 5Model three domains on paper in 30 minutes each (for example a rate limiter, a library loan system and a campaign scheduler): types, invariants, state transitions and data owners.
- 6Take a module from your own code, draw its import graph, and remove one dependency from domain logic onto infrastructure.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
MIT 6.102 Software Construction (readings)Start here
MIT EECSSection: 07: Abstraction Functions & Rep Invariants
Precise vocabulary for invariants, rep exposure and representation independence, with short exercises; read 06 (Abstract Data Types) first.
Open(opens in a new tab)Link responded, 5 Oct 2026On the Criteria To Be Used in Decomposing Systems into ModulesStart here
Communications of the ACM (ACM Digital Library)The classic argument for decomposing a system by hidden design decisions rather than by processing steps; short enough to read in one sitting.
Open(opens in a new tab)Not checkable automatically, 5 Oct 2026Designing Data-Intensive Applications
O'Reilly Media (author's book site)Section: Ch. 2: Data Models and Query Languages
Relational, document and graph models, and how relationships and access patterns shape the choice.
Open(opens in a new tab)Link responded, 5 Oct 2026BoundedContext
martinfowler.comShort explanation of bounded contexts for dividing a large domain model into consistent parts with explicit translations.
Open(opens in a new tab)Link responded, 5 Oct 2026Hexagonal Architecture (Ports and Adapters)
Alistair Cockburn's websitePorts and adapters as a way to keep domain logic independent of databases, frameworks and external services, and testable with fakes.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Practice on paper with a timer. For each of three small domains, write the types with their invariants, the state transitions, the owner of every table and one likely future change, then check whether that change touches one module or several. Afterwards, refactor one real module so its domain logic no longer imports the database client or the HTTP framework, and test it with an in-memory fake.
No external problems are linked; follow the guidance above.
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1What is rep exposure, and how do immutability or defensive copying prevent it?
- Question 2When should a concept be a value object rather than an entity?
- Question 3What goes wrong when two services write the same table?
- Question 4Which way should dependencies point between domain logic and the database layer, and why?
- Question 5What is a bounded context, and when would you translate between two models instead of merging them?
- Question 6How can you tell that a module boundary is in the wrong place?
You’re done when you can
- Model an unseen small domain in 30 minutes with explicit invariants, state transitions and data owners.
- Write a rep invariant and abstraction function for a type you implemented and point out any rep exposure.
- Explain in two minutes, with an example, why decomposing by likely change beats decomposing by processing steps.
- Name the single writer of each table in a project you know, and flag any table with several writers.
Finish the lesson
Unlocks next: Testing strategy


