Object design interview questions
These are the questions that come up in low-level design (LLD) interviews: classes, interfaces, patterns, and the principles behind them. Each answer gives the standard version first, the one interviewers expect to hear. Then, where it matters, where it breaks down: when the textbook answer is out of date, overused, or wrong for the language you're writing in.
Knowing where an idea stops working is what separates a memorized answer from experience. Interviewers notice.
What low-level design is
What is low-level design, and why does it matter?
Low-level design turns a high-level plan ("there's a payment service") into something you could code: the classes, their responsibilities, the methods and their inputs and outputs, and how objects talk to each other. A parking lot design, for example, decides that there are ParkingLot, Level, Spot, and Ticket objects, what each one knows, and which one decides whether a car fits.
Doing this before coding removes ambiguity. Two engineers can build parts that fit together, and reviewers can spot a bad boundary before it's written.
Where it breaks down: a full design up front goes stale fast when requirements are still moving. Design the parts that are expensive to change later, such as data models, public APIs, and boundaries between teams. Let the internals take shape as you write them.
How is low-level design different from high-level design?
High-level design (HLD) decides the big pieces: services, databases, caches, queues, and how requests flow between them. Low-level design decides what's inside one piece: classes, methods, and data structures. For a food delivery app, HLD says there's an order service and a payment service. LLD designs the Order, OrderItem, and PaymentProcessor classes inside them.
In interviews, LLD questions sound like "design a parking lot" or "design an elevator". HLD questions sound like "design Twitter". The system design questions page covers the HLD side.
What is object-oriented design, and why use it?
Object-oriented design (OOD) models a system as objects that combine data with the behavior that acts on it, each with a clear job. A bank has Account objects that know their balance and enforce their own rules ("no overdraft"), instead of every part of the program reaching in to change balances directly.
The benefit is that rules live next to the data they protect, so there's one place to change them.
Where it breaks down: not everything is best modeled as an object. A calculation like an order total is just a function. Data passed between services is just data. A pipeline that transforms records is clearer as a series of functions. Modern code mixes styles: objects where there's state with rules to protect, plain functions and data everywhere else.
Object-oriented basics
What are the four pillars of object-oriented programming?
- Encapsulation: an object keeps its data private and exposes methods, so it can enforce its own rules.
- Abstraction: callers use a simple interface without knowing how it works inside.
- Inheritance: a class reuses and extends another class's code.
- Polymorphism: different types respond to the same call in their own way.
A banking app might have SavingsAccount and CheckingAccount share code through a base Account class (inheritance) while each calculates interest differently (polymorphism).
Where it breaks down: this list dates from the 1990s, when inheritance was the main way to share code. Today it's the least important of the four. Go and Rust have no class inheritance at all. They get polymorphism from interfaces (Go) and traits (Rust), and reuse from composition. Python and Java still have inheritance, but most style guides say to prefer composition, and keep hierarchies shallow. If you list the four pillars in an interview, add that you'd reach for interfaces and composition before inheritance. That one sentence shows current judgment.
What's the difference between abstraction and encapsulation?
Abstraction is about what you show: callers see car.start(), not the ignition sequence. Encapsulation is about what you protect: the engine's internals can't be changed from outside, so the car stays in a valid state.
Abstraction is usually built with interfaces or abstract classes. Encapsulation is usually built with private fields and methods that check the rules.
Where it breaks down: encapsulation is often taught as "make every field private and add a getter and setter". A setter for every field protects nothing; it's a public field with extra steps. Real encapsulation means the object's methods are the rules: account.withdraw(50) checks the balance, while account.set_balance(...) lets anyone break it. Python has no private fields at all. It uses a leading underscore (_balance) as a "don't touch" convention, and @property when you need a check later, without changing callers.
What is polymorphism?
Polymorphism means one call works on many types, with each type supplying its own behavior. payment.charge(amount) might charge a card, debit a bank account, or call PayPal, and the caller doesn't care which.
The textbook splits it into two kinds. Compile-time polymorphism is method overloading: several methods with one name and different parameter types. Run-time polymorphism is method overriding: a subclass replaces a parent's method.
Where it breaks down: that two-way split is Java-centric. Python has no overloading; a second definition with the same name replaces the first. It uses default arguments, or functools.singledispatch when you really need to choose by type. And the list leaves out two kinds used constantly today. Duck typing: in Python, any object with a charge method works, no shared base class needed. Generics: one function written for many types, like list[T] or Rust's Vec<T>. Interviewers rarely penalize the textbook answer, but adding these shows you write real code.
Inheritance or composition: which should you prefer?
Inheritance says a class is a kind of another: a Dog is an Animal. Composition says a class has a part: a Car has an Engine. The usual advice is to prefer composition, because it's more flexible and less tightly coupled.
Here's why. With inheritance, a subclass depends on its parent's internals, so a change to the parent can break every child. With composition, you can swap the part (an ElectricEngine for a GasEngine) without touching the car, and even swap it while the program runs.
class Car:
def __init__(self, engine):
self.engine = engine # Any object with a start() method.
def start(self):
self.engine.start()
Where it breaks down: the advice is right, and the industry moved further than the textbook says. Go and Rust dropped class inheritance entirely: they share behavior through interfaces and traits, and share code through composition. Inheritance still earns its place in a few spots: shallow hierarchies inside frameworks (a Django model, an exception type) and true "is a" relationships where every subclass keeps every promise of the parent. If a subclass has to disable or change a parent method's meaning, it shouldn't inherit. That's the Liskov substitution rule.
What's the difference between an interface and an abstract class?
An interface defines a contract: the methods a class must provide, with no shared code or data. An abstract class can do more: it can hold fields and implement some methods, leaving others for subclasses. A class can implement many interfaces but, in Java or C#, extend only one class.
Use an interface for a capability many unrelated types share, such as "can be compared" or "can send a notification". Use an abstract class when subclasses truly share data and code.
Where it breaks down: the line has blurred. Since Java 8, interfaces can have default method implementations. Rust traits have default methods too. Python has two tools: abc.ABC (an abstract class that classes must explicitly inherit) and typing.Protocol (an interface that any class with the right methods matches, no inheritance needed). In modern Python, Protocol is usually the better choice for a contract, because it doesn't force a shared base class.
from typing import Protocol
class Notifier(Protocol):
def send(self, user_id: str, message: str) -> None: ...
class EmailNotifier: # Matches Notifier without inheriting from it.
def send(self, user_id: str, message: str) -> None: ...
What role do interfaces play in low-level design?
An interface is a contract between parts. Code that depends on a Notifier interface works with EmailNotifier, SmsNotifier, or PushNotifier, and a test can pass in a fake one. That's what lets you add a new kind of notification without changing the code that sends them.
Where it breaks down: an interface with exactly one implementation, and no test that needs a fake, is just an extra file. Add the interface when the second implementation shows up, or when a test needs to swap it out. Until then, depend on the concrete class.
What's the difference between association, aggregation, and composition?
All three describe one object referring to another. They differ in who owns whom:
| Relationship | Ownership | Example |
|---|---|---|
| Association | None; they just know each other | A teacher teaches students |
| Aggregation | Weak; the part can live on its own | A department has employees |
| Composition | Strong; the part dies with the whole | An order has line items |
If you delete a department, the employees still exist. If you delete an order, its line items go too.
Where it breaks down: in Python, Java, or JavaScript, all three are just references; the language doesn't enforce any difference. The distinction matters in two practical places. Databases: composition becomes ON DELETE CASCADE (delete the order, delete its items), while aggregation doesn't. Rust and C++: the language makes ownership real. In Rust, an owned field is composition and a borrowed reference is association. Outside those, the three names are UML vocabulary; know them, but don't spend long on them.
Design principles
What are the SOLID principles?
Five principles for organizing classes:
- Single responsibility: a class should have one reason to change.
- Open/closed: add new behavior by adding code, not editing working code.
- Liskov substitution: a subclass must work anywhere its parent does.
- Interface segregation: don't make callers depend on methods they don't use.
- Dependency inversion: high-level rules shouldn't depend on low-level details; both should depend on abstractions.
Where it breaks down: each one has a cost when overused, from eight tiny classes for a 40-line feature to six wrapper layers around one API call. The design principles page covers each with its failure mode. The strongest interview answer: "I treat SOLID as heuristics, not rules, and I wait until I see real variation before adding an abstraction."
What is DRY?
"Don't repeat yourself": every piece of knowledge should live in one place. If the rule for a valid email is written in five places, a fix has to happen five times, and one will be missed. Put it in one function and call it.
Where it breaks down: DRY is about knowledge, not text. Two pieces of code that look the same but change for different reasons aren't duplicates. Merging them couples things that should move independently, and the shared function sprouts flags for each caller. Sandi Metz's line is worth quoting: "duplication is far cheaper than the wrong abstraction." A common rule of thumb is to wait for the third copy before extracting.
What are KISS and YAGNI?
KISS ("keep it simple"): choose the simplest design that meets the requirements. An if statement beats three design patterns when an if is enough.
YAGNI ("you aren't gonna need it"): build only what's required now. Don't build five payment providers when only one is needed.
Where it breaks down: YAGNI doesn't mean ignore what you know is coming. Some decisions are cheap to add later, like a second payment provider behind an existing function. Others are expensive to retrofit: storing times in UTC, using IDs that won't collide across regions, versioning a public API, and security. Spend effort up front on the expensive-to-change things, and defer the cheap ones.
What is tight coupling, and why is it a problem?
Two parts are tightly coupled when changing one forces changes in the other. If PaymentService creates a PayPalClient inside itself, you can't switch providers, or test without calling PayPal, without editing PaymentService.
Loosen it by passing the dependency in from outside, often behind an interface.
Where it breaks down: some coupling is fine. Code that always changes together, like a class and its private helper, should be coupled. Loose coupling has costs: more files, more indirection, and behavior that's harder to follow. Loosen the couplings that hurt: to outside services, databases, the clock, and things you need to fake in tests.
What is dependency injection?
Dependency injection means a class receives what it needs instead of creating it. Instead of self.db = PostgresDatabase() inside UserService, the caller passes a database in: UserService(db). Now a test can pass a fake database, and production can pass the real one.
class UserService:
def __init__(self, db):
self.db = db # Passed in, so a test can hand it a fake.
Where it breaks down: dependency injection is a simple idea that's often buried under frameworks. Passing an argument to a constructor is all it is. Java and C# projects often use containers that wire objects together automatically; in Python, Go, and JavaScript that's rarely needed. Don't inject things that never vary, like a pure helper function or the standard library's math.
Why does modular design matter?
A modular system splits into parts that each handle one area, with clear boundaries: users, inventory, payments, and orders. Each module can be understood, tested, and changed mostly on its own, and different teams can own different modules.
Where it breaks down: modular doesn't mean microservices. You can have strong module boundaries inside one program, a "modular monolith", and get most of the benefit without network calls, distributed failures, or separate deployments. Split into services only when you need independent scaling or deployment, not just to get modules.
What are code smells, and how do you fix them?
A code smell is a sign of a design problem that isn't a bug yet: a 300-line method, copied code, a class that handles login, payments, and email (a "god class"), or long if chains on a type field. Fixes are refactorings: extract a function, split the class, move behavior next to its data.
Where it breaks down: a smell is a hint, not a verdict. A long function that reads top to bottom with no branching can be clearer than ten tiny ones. An if chain over three stable cases is fine. Refactor when the smell is actually slowing changes down, not because a list said so.
Design patterns
What are design patterns, and what kinds are there?
Design patterns are named, reusable solutions to common design problems. They give a team shared vocabulary: "make it a strategy" says a lot in three words. The classic 1994 "Gang of Four" book groups them into three kinds:
- Creational: how objects get made. Singleton, Factory, Builder.
- Structural: how objects fit together. Adapter, Facade, Decorator, Proxy.
- Behavioral: how objects interact. Observer, Strategy, Command, Chain of Responsibility.
Where it breaks down: many patterns are workarounds for missing language features, mostly Java's in the 1990s. In a language with first-class functions, Strategy is a function you pass in, Command is a function or closure you store, and Observer is a list of callbacks. In Python, Singleton is a module. Name the pattern in an interview, then say what it looks like in the language you're using. That shows you understand the problem the pattern solves, not just its UML diagram.
When should you avoid design patterns?
When a simpler design does the job. A Strategy pattern for one tax rule is three classes doing what one function does. Patterns earn their cost when there's a real recurring problem: several interchangeable algorithms, objects that must react to changes, operations that must be undone.
How to choose: name the problem first ("callers need to swap the pricing rule at run time"). Then pick the simplest thing that solves it. Often that's a pattern; often it's a function or a dictionary.
Where it breaks down: the opposite mistake exists too. Hand-rolling something a well-known pattern solves, like your own undo system without the Command pattern's structure, often reinvents it badly. Patterns aren't good or bad. Use them when they fit.
Can you combine design patterns?
Yes, and real systems usually do. Model-view-controller (MVC) uses Observer so views update when the model changes, Strategy for swappable behavior, and Factory to build objects. Each pattern handles one concern.
Where it breaks down: stacking patterns multiplies indirection. A factory that builds a strategy chosen by a registry, wrapped in a decorator, can take a new reader an hour to follow. Every pattern you add should solve a problem you can name.
What is the Singleton pattern, and what's wrong with it?
A singleton guarantees a class has exactly one instance, with a global way to get it. The classic version hides the constructor and adds a static get_instance(). It's used for configuration, a logger, or a database connection pool.
Its problems: a singleton is global state. Any code can reach it, so it's hard to tell what depends on what. Tests can't easily swap in a fake, and one test's changes leak into the next. And it couples every caller to one concrete class.
Where it breaks down: in Python, the pattern is mostly unnecessary. A module is created once, so a module-level object is already a singleton:
# config.py
settings = {"timeout_seconds": 30} # Every import shares this one dictionary.
Better still, create the one instance at startup and pass it to whatever needs it, which is dependency injection. You still get one instance, without the global access.
What is the Factory pattern?
A factory creates objects without the caller knowing the exact class. The caller asks for "a payment method for UPI" and gets back an object with a charge method. Use it when the type isn't known until run time, or when building an object is complicated enough to deserve one home.
def make_payment_method(kind):
methods = {"card": CardPayment, "upi": UpiPayment, "wallet": WalletPayment}
return methods[kind]()
Where it breaks down: in Python, a factory is usually just a function or a dictionary like the one above. A PaymentMethodFactory class with one method adds a class for no reason. If there's only one type, call its constructor.
What's the difference between a static factory method and the Factory pattern?
A static factory method is a method on a class that returns an instance: User.from_json(data) or Python's datetime.fromtimestamp(t). It gives creation a descriptive name and can validate inputs. The Factory pattern is a separate object or function in charge of deciding which class to build.
Where it breaks down: a common claim is that static factory methods "can't be polymorphic". That's wrong: a static factory can return a subtype. Java's List.of(...) returns different internal list classes depending on how many items you pass. In Python, a @classmethod factory that builds with cls(...) returns whichever subclass it's called on. Use a named constructor first, and a separate factory only when the choice of class is complicated.
When should you use Abstract Factory instead of Factory Method?
Use Abstract Factory when you need a whole family of related objects that must match. A UI toolkit might need buttons, menus, and scroll bars that all look like Windows, or all look like macOS. An Abstract Factory has one method per product and guarantees the set matches. Factory Method creates one kind of object, with subclasses deciding which.
Where it breaks down: Abstract Factory is rare outside cross-platform UI toolkits and plugin systems. If you reach for it in an interview, be ready to say what the "families" are and why mixing them would be a bug. Otherwise it's speculative structure.
What is the Strategy pattern?
Strategy puts each version of an algorithm behind the same interface, so you can pick one at run time. A checkout can charge by card, UPI, or PayPal through one charge call, or a router can choose the fastest or shortest route.
Where it breaks down: with first-class functions, a strategy is a function. Python's own sorted takes its strategy as an argument:
words = ["banana", "Apple", "cherry"]
sorted(words, key=str.lower) # The key function is the strategy.
Use classes when a strategy needs its own settings or several methods. Otherwise, pass a function.
What is the Observer pattern?
An observer subscribes to a subject and gets notified when it changes. A stock's price changes, and every trader watching it hears about it. The subject keeps a list of observers and calls each one, without knowing what they do. This is how UI events, notifications, and many messaging systems work.
Where it breaks down: observers bring three common bugs. Memory leaks: a subject holds references to observers, so an observer nobody else uses can never be freed unless it unsubscribes. Hidden control flow: one change can set off a chain of updates that's hard to trace. Ordering and errors: what happens if the third observer raises an exception? Across services, the same idea becomes a message queue or event bus, which brings delivery and ordering questions of its own. See event-driven architecture.
How does the Command pattern support undo and redo?
The Command pattern turns each action into an object that knows how to do itself and how to undo itself. A text editor stores "insert 'hello' at position 10" as a command. To undo, pop the last command off a history stack and call its undo. To redo, keep undone commands on a second stack.
Where it breaks down: undo is only as good as each command's undo, and some actions can't be cleanly reversed, like sending an email or charging a card. For those, the "undo" is a new action, a refund. For complex state, storing snapshots (the Memento pattern) can be simpler than writing a reverse for every command. Collaborative editors need much more, since several people's commands interleave.
What is the Chain of Responsibility pattern?
A request passes through a chain of handlers, and each one either handles it or passes it on. An expense approval goes to a manager, then a director if it's too big, then the CEO. The sender doesn't know who will handle it.
Where it breaks down: you've used this pattern if you've used web middleware. Django, Express, and most web frameworks run each request through a chain: logging, authentication, rate limiting, then your code. The risk is that a request falls off the end with nobody handling it, or that the order of handlers matters in ways nobody wrote down.
What's the difference between Proxy and Decorator?
Both wrap an object behind the same interface. A proxy controls access: it checks permissions, loads the real object only when needed, or forwards calls to another machine. A decorator adds behavior: logging, compression, or caching on top of the original.
Where it breaks down: Python's @decorator syntax is related but not the same thing. It wraps functions, not objects, and is used for both purposes (@login_required acts like a proxy, @lru_cache like a decorator). In practice the line between the two patterns is intent, not structure. Say which one you mean and why.
What is the Null Object pattern?
Instead of None, you use an object with the same interface that does nothing. A NullLogger has a log method that ignores its input, so code can always call logger.log(...) without checking for None first.
Where it breaks down: a null object can hide bugs. If a missing logger is fine, great. But a missing payment method silently doing nothing is a bug you won't see. Modern languages offer a better tool for "might be missing": optional types, like Optional[T] in Python, Option<T> in Rust, and Optional in Java. The type checker forces callers to handle the missing case. Use a null object only when "do nothing" really is the right behavior.
Choosing data structures and algorithms
What decides which data structure to use?
The operations you need, and how fast each must be. Ask what's accessed most: lookup by key (a hash map), first-in-first-out order (a queue), always the smallest item (a heap), sorted order with range queries (a balanced tree), relationships between things (a graph). Then consider memory limits and whether several threads use it at once.
A chat app might use a queue for messages waiting to send, a hash map from user ID to user, and a graph for who follows whom. The data structures section covers each in depth.
Where it breaks down: big-O isn't the whole story. For a few hundred items, a plain list is often faster than a hash map or tree, because it's compact and cache-friendly. Measure before optimizing small cases.
What factors decide which sorting algorithm to use?
Data size, whether it fits in memory, whether equal items must keep their order (stability), and how sorted the data already is. For data bigger than memory, use external merge sort: sort chunks that fit, then merge them.
Where it breaks down: in practice, you almost always use the language's built-in sort. Python's sort is a stable merge sort variant that's very fast on partly sorted data. Writing your own quicksort is an exercise, not a production choice. The real decisions are what key to sort by, whether you need stability, and whether you need to sort at all. A heap or a single pass can be cheaper when you only need the top few items.
How does database indexing speed up queries?
An index is an extra sorted structure, usually a B-tree, that lets the database find rows without scanning the whole table. With an index on user_id, finding one user's orders takes a few steps instead of reading every order.
Where it breaks down: indexes aren't free. Every insert and update must also update each index, and indexes take disk and memory. An index on a column with few distinct values, like a true/false flag, rarely helps. And column order in a combined index matters: an index on (user_id, created_at) helps queries that filter by user_id, but not ones that filter only by created_at. Index the queries you actually run.
What is database normalization, and when should you break it?
Normalization organizes tables so each fact is stored once. Instead of copying a customer's address into every order, store it once in customers and point to it from orders. Updates happen in one place, and the data can't contradict itself.
Where it breaks down: normalized data needs joins to read, and joins get expensive at scale or across machines. Read-heavy systems often denormalize on purpose: copy the customer's name into the order so an order page needs one query. That trades simpler, faster reads for harder writes, since now the copies must be kept in sync. Normalize first, then denormalize specific hot paths when you've measured the need.
What are the important principles of database design?
Model the real entities and their relationships (users, orders, products), normalize to avoid contradictions, add constraints (foreign keys, NOT NULL, unique) so the database rejects bad data, choose the right column types, and index the queries you'll run.
Where it breaks down: constraints are your last line of defense, and many teams skip them "for flexibility", then find bad data later. Keep them. On the other side, a schema designed for every possible query is overbuilt. Design for the queries you know, and plan for change with migrations.
Concurrency, APIs, and operations
How do you handle concurrency in a multi-threaded system?
When several threads change shared data, their steps can interleave and corrupt it. Two withdrawals both read a $100 balance, both subtract $80, and both succeed. That's a race condition. Fix it by making the read-check-write happen as one step: hold a lock around it, use an atomic operation, or let the database do it.
# One atomic database statement: the check and the update can't interleave.
# UPDATE accounts SET balance = balance - 80 WHERE id = 7 AND balance >= 80
Watch for deadlocks: two threads each holding one lock and waiting for the other's. Always taking locks in the same order prevents them.
Where it breaks down: locks in one program don't help when several servers share the data. Then the database, or a distributed lock, has to provide the guarantee. In Python, the global interpreter lock doesn't make your code thread-safe: count += 1 is several steps, and another thread can run in between. Often the simplest fix is not sharing: give each thread its own data, or pass messages through a queue.
How do you handle versioning and backward compatibility?
For APIs, keep the old version working while clients move to the new one: run /api/v1/users and /api/v2/users side by side, announce a deprecation date, and remove v1 only when traffic is gone. For databases, change in steps: add the new column, write to both, backfill old rows, switch reads, then drop the old column.
Where it breaks down: most changes don't need a new version. Adding an optional field or a new endpoint breaks no one, as long as clients ignore fields they don't know. Save new versions for real breaking changes, because every live version is code you have to maintain.
How do you design secure authentication and authorization?
Authentication checks who you are; authorization checks what you're allowed to do. For authentication, store passwords with a slow hashing function like bcrypt or Argon2, offer multi-factor login, and use a standard protocol such as OAuth 2.0 or OpenID Connect instead of inventing your own. For authorization, give users roles (role-based access) or check attributes per request (attribute-based access), and check on the server for every request.
Where it breaks down: a common design gives users a signed token (a JWT) that each service can verify on its own. The catch: a token can't easily be revoked before it expires, so a stolen or logged-out token keeps working. Keep access tokens short-lived, with longer-lived refresh tokens you can revoke. And never write your own cryptography.
How do you design logging and monitoring?
Log in a structured format, like JSON, with levels (debug, info, warning, error), and send all logs to one central place. Track metrics such as request rate, error rate, latency, CPU, and memory, with dashboards and alerts. Give each request an ID that's passed from service to service, so you can follow one request across the system (distributed tracing).
Where it breaks down: logging everything is expensive and noisy, and logs often leak personal data like emails or tokens. Alert on what users feel, such as errors and slow responses, not on every CPU spike, or people learn to ignore alerts. Metrics with too many label values, like one series per user ID, can overwhelm a monitoring system.
Diagrams
Which UML diagrams matter, and what do they show?
UML (Unified Modeling Language) is a standard set of diagrams. The common ones:
- Class diagram: classes, their fields and methods, and how they relate.
- Sequence diagram: messages between parts over time. For a login: the UI sends credentials to a login controller, which asks an authentication service, which checks the database, and the result flows back.
- State diagram: the states an object moves through, and the events that move it. A payment goes from pending, to processing, to completed or failed.
- Use case and activity diagrams: who uses the system for what, and step-by-step workflows.
Behavioral diagrams (sequence, state, activity) show how things change over time; structural ones (class) show what exists.
Where it breaks down: almost nobody draws formal UML at work anymore. What survives is informal: boxes and arrows for classes, and sequence diagrams on a whiteboard to walk through a request. State diagrams are the most underrated: if an object has a lifecycle, writing its allowed transitions down catches bugs before they ship. In code, that's often just a table:
ALLOWED = {
"pending": {"processing", "failed"},
"processing": {"completed", "failed"},
"completed": set(),
"failed": set(),
}
def move(state, new_state):
if new_state not in ALLOWED[state]:
raise ValueError(f"can't go from {state} to {new_state}")
return new_state
In an interview, draw what helps you explain. Don't spend minutes getting UML arrowheads right.