Decorator — Hands-On Tasks¶
Each task includes a brief, the interface, and a reference solution. Try first; check after.
Table of Contents¶
- Task 1: Coffee with toppings
- Task 2: Logging + Caching repository
- Task 3: HTTP middleware chain
- Task 4: Retry with idempotency
- Task 5: Circuit breaker decorator
- Task 6: Rate limiter decorator
- Task 7: Stream pipeline (compress + buffer + write)
- Task 8: Conditional decoration by config
- Task 9: Trace context propagation
- Task 10: Refactor inline logging into Decorator
- How to Practice
Task 1: Coffee with toppings¶
Brief. Build a Coffee interface; Plain is the base; Milk, Sugar, Whip are decorators.
Solution (Python)¶
class Coffee:
def cost(self) -> int: ...
def desc(self) -> str: ...
class Plain(Coffee):
def cost(self): return 30
def desc(self): return "coffee"
class Milk(Coffee):
def __init__(self, inner): self._inner = inner
def cost(self): return self._inner.cost() + 10
def desc(self): return self._inner.desc() + " + milk"
class Sugar(Coffee):
def __init__(self, inner): self._inner = inner
def cost(self): return self._inner.cost() + 5
def desc(self): return self._inner.desc() + " + sugar"
class Whip(Coffee):
def __init__(self, inner): self._inner = inner
def cost(self): return self._inner.cost() + 15
def desc(self): return self._inner.desc() + " + whip"
c = Whip(Sugar(Milk(Plain())))
print(c.desc(), c.cost()) # coffee + milk + sugar + whip 60
Task 2: Logging + Caching repository¶
Brief. A repo that fetches users; add caching and logging via decorators.
Solution (Go)¶
type UserRepo interface {
Get(id string) (User, error)
}
type postgresRepo struct{ db *sql.DB }
func (p *postgresRepo) Get(id string) (User, error) { /* ... */ }
type cacheRepo struct {
inner UserRepo
cache sync.Map
ttl time.Duration
}
func (c *cacheRepo) Get(id string) (User, error) {
if v, ok := c.cache.Load(id); ok { return v.(User), nil }
u, err := c.inner.Get(id)
if err == nil { c.cache.Store(id, u) }
return u, err
}
type logRepo struct{ inner UserRepo; logger Logger }
func (l *logRepo) Get(id string) (User, error) {
l.logger.Info("get", "id", id)
return l.inner.Get(id)
}
repo := &logRepo{inner: &cacheRepo{inner: &postgresRepo{db: db}, ttl: time.Minute}, logger: log}
Task 3: HTTP middleware chain¶
Brief. Express-style middleware in Go: Logging + RequestID + Auth wrapping a handler.
Solution¶
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mw ...Middleware) http.Handler {
for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) }
return h
}
func Logging(l Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
l.Info("http", "path", r.URL.Path, "duration", time.Since(start))
})
}
}
func RequestID() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.NewString()
w.Header().Set("X-Request-ID", id)
r = r.WithContext(context.WithValue(r.Context(), "rid", id))
next.ServeHTTP(w, r)
})
}
}
func Auth(verify func(*http.Request) bool) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !verify(r) { http.Error(w, "unauthorized", 401); return }
next.ServeHTTP(w, r)
})
}
}
handler := Chain(realHandler, Logging(log), RequestID(), Auth(verifyToken))
Task 4: Retry with idempotency¶
Brief. Wrap a service with retry; ensure each retry uses the same idempotency key.
Solution (Java)¶
public class IdempotentRetry implements Service {
private final Service inner;
private final int maxTries;
private final Backoff backoff;
@Override
public Result call(Request req) throws Exception {
Request keyed = req.idempotencyKey() != null ? req
: req.withIdempotencyKey(UUID.randomUUID().toString());
Throwable last = null;
for (int i = 0; i < maxTries; i++) {
try { return inner.call(keyed); }
catch (TransientException e) {
last = e;
if (i < maxTries - 1) Thread.sleep(backoff.delay(i));
}
}
throw new RetriesExhaustedException(last);
}
}
Task 5: Circuit breaker decorator¶
Brief. A simple circuit breaker: opens after N consecutive failures; rejects calls until cooldown elapses; half-open after cooldown to test recovery.
Solution (Go)¶
type State int
const (
Closed State = iota
Open
HalfOpen
)
type CircuitBreaker struct {
inner Service
threshold int
cooldown time.Duration
failures int
openedAt time.Time
state State
mu sync.Mutex
}
func (cb *CircuitBreaker) Call(ctx context.Context, req Request) (Result, error) {
cb.mu.Lock()
if cb.state == Open {
if time.Since(cb.openedAt) > cb.cooldown {
cb.state = HalfOpen
} else {
cb.mu.Unlock()
return Result{}, errors.New("circuit open")
}
}
cb.mu.Unlock()
r, err := cb.inner.Call(ctx, req)
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
if cb.failures >= cb.threshold {
cb.state = Open
cb.openedAt = time.Now()
}
} else {
cb.failures = 0
cb.state = Closed
}
return r, err
}
Task 6: Rate limiter decorator¶
Brief. Token bucket: allow N permits/second; block until permit available.
Solution (Java, simplified)¶
public class RateLimitedService implements Service {
private final Service inner;
private final RateLimiter limiter; // e.g., Guava RateLimiter
public RateLimitedService(Service inner, double permitsPerSecond) {
this.inner = inner;
this.limiter = RateLimiter.create(permitsPerSecond);
}
@Override
public Result call(Request req) {
limiter.acquire();
return inner.call(req);
}
}
Task 7: Stream pipeline (compress + buffer + write)¶
Brief. Java-style decorated streams.
Solution (Java)¶
try (var out = new ObjectOutputStream(
new GZIPOutputStream(
new BufferedOutputStream(
new FileOutputStream("snapshot.gz"))))) {
out.writeObject(snapshot);
}
Each layer adds behavior. Reading reverses: ObjectInputStream(GZIPInputStream(BufferedInputStream(FileInputStream))).
Task 8: Conditional decoration by config¶
Brief. Build a service stack based on a config object; some decorators are off in dev.
Solution (Python)¶
def maybe(d, condition, **kwargs):
return d if not condition else lambda inner: d(inner, **kwargs)
def build_processor(cfg):
p = StripeProcessor(cfg.stripe_client)
if cfg.retry_enabled:
p = RetryProcessor(p, max_tries=cfg.max_tries)
if cfg.cb_enabled:
p = CircuitBreaker(p, threshold=5, cooldown=30)
p = MetricsProcessor(p, cfg.metrics) # always on
return p
Task 9: Trace context propagation¶
Brief. A decorator that creates a span and propagates context.
Solution (Go)¶
type tracedService struct {
inner Service
tracer Tracer
name string
}
func (t *tracedService) Call(ctx context.Context, req Request) (Result, error) {
span, ctx := t.tracer.StartSpan(ctx, t.name)
defer span.End()
r, err := t.inner.Call(ctx, req)
if err != nil {
span.SetStatus(codes.Error, err.Error())
}
return r, err
}
Task 10: Refactor inline logging into Decorator¶
Brief. Take this class with embedded logging:
public class UserService {
private final UserRepo repo;
private final Logger log;
public User get(String id) {
log.info("get({})", id);
try {
User u = repo.get(id);
log.info("got user {}", u.email());
return u;
} catch (Exception e) {
log.error("failed", e);
throw e;
}
}
}
Refactor logging into a Decorator.
Solution¶
public class UserService {
private final UserRepo repo;
public User get(String id) { return repo.get(id); } // pure
}
public class LoggingUserService implements UserService {
private final UserService inner;
private final Logger log;
public User get(String id) {
log.info("get({})", id);
try {
User u = inner.get(id);
log.info("got user {}", u.email());
return u;
} catch (Exception e) {
log.error("failed", e);
throw e;
}
}
}
UserService svc = new LoggingUserService(new RealUserService(repo), logger);
UserService is single-responsibility; logging lives in its own class.
How to Practice¶
- Try each task. Don't peek until you have something working.
- Stack the decorators. Don't just write one — stack 2-3 to feel the composition.
- Test in isolation. Each decorator gets its own unit test against a fake inner.
- Test the order. Build two stacks with reversed orderings; show different behavior.
- Add idempotency to retry tasks. Pass and assert the key.
- Profile a deep stack. 5 trivial layers under load — measure overhead.
- Refactor real code. Find a class with embedded cross-cutting concerns; extract one decorator at a time.
← Back to Decorator folder · ↑ Structural Patterns · ↑↑ Roadmap Home
Next: Decorator — Find the Bug