Protocols in System Design: How Applications Actually Talk to Each Other

A practical system design guide to TCP, UDP, HTTP, HTTPS, REST, real-time communication, GraphQL, and gRPC through a hypothetical travel-itinerary platform.

Listen to this article

0:0022:07

Checking audio support

A travel route on a phone connected to cloud, API, database, real-time, GraphQL, and server systems.

One hypothetical travel platform, several communication patterns, and a protocol chosen for each job.

A traveller opens a shared Kerala itinerary on a phone. The page loads a route, photos, and practical notes. A moment later, the traveller records interest in an activity, the coordinator sees an updated count, and the group receives a live route alert. The screen feels simple because many communication decisions are hidden underneath it.

Important context: the travel-itinerary platform in this guide is a hypothetical system-design example. It is not a description of a currently bookable Travel With Snigdha product. We use it because one familiar journey can demonstrate reliable writes, cached reads, live updates, two-way messaging, flexible dashboards, and internal service calls.

Protocols are the agreements that let each part of that system communicate. They define how a connection starts, how bytes move, how a request is shaped, how errors are represented, whether old data may be reused, and what happens when a network fails. The useful skill is not memorising protocol names. It is matching a communication pattern to a product requirement.

1. Start with the conversation, not the protocol

A system-design answer becomes clearer when it names the conversation first. Is the user reading public content, submitting important state, receiving occasional updates, exchanging messages in both directions, or calling another service inside a private network? Each case has a different balance of reliability, latency, direction, cacheability, and operational cost.

  • Public itinerary page: a cacheable read that should be fast and resilient.

  • Activity-interest form: a reliable state-changing write that must not duplicate on retry.

  • Live traveller count: mostly one-way updates from server to browser.

  • Travel-group chat: a persistent two-way conversation.

  • Coordinator dashboard: several related fields with different data shapes.

  • Internal notification call: a typed service-to-service contract.

  • Vehicle location: freshness matters more than replaying an old point.

This gives us a practical protocol stack. TCP and UDP move data between machines. TLS protects it in transit. HTTP supplies the web's request-response language. REST organises public resources and methods. Server-Sent Events and WebSockets change the timing and direction of live communication. GraphQL lets a client select a response shape. gRPC gives controlled services a strongly typed contract.

2. TCP and UDP: reliability versus freshness

TCP is connection-oriented. It establishes shared state, orders bytes, acknowledges delivery, retransmits missing data, and controls congestion. Most HTTPS traffic uses TCP in HTTP/1.1 and HTTP/2 because pages, form submissions, authentication, and database calls need a reliable stream.

text
Client -> Server: SYN
Server -> Client: SYN-ACK
Client -> Server: ACK

The three-way handshake costs a small amount of time, but it gives both sides a shared context. If a traveller submits an activity-interest response, losing half the request body is unacceptable. TCP handles packet ordering and retransmission below the application, so the API receives a coherent byte stream.

UDP sends independent datagrams without creating that reliable stream. It does not guarantee delivery or ordering. That is useful when the latest state is more valuable than an old state. If a trip vehicle sends a new location every few seconds, retrying a point from a minute ago may be worse than dropping it and displaying the latest point.

UDP is not automatically better because it can have lower overhead. A missing payment, login, or form submission is a correctness defect. A missing telemetry point may be tolerable. The product's failure model decides whether reduced coordination is worth the tradeoff.

3. HTTP, HTTPS, and caching

HTTP defines methods, headers, status codes, and bodies. HTTPS is HTTP protected by TLS, which encrypts data in transit, checks integrity, and helps the client verify the server's identity. Public pages, admin sessions, traveller details, and any future transaction flow should all use HTTPS.

http
GET /dev-diaries/protocols-in-system-design-how-applications-talk-to-each-other-1082.html HTTP/1.1
Host: blog.snigdhainvitations.com
Accept: text/html
User-Agent: Browser
http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=300

<html>...</html>

HTTP is stateless: a page read and a later form submission are separate requests. Applications create continuity with sessions, cookies, signed tokens, and database records. This stateless boundary helps scale because a load balancer can send consecutive requests to different servers when shared state lives outside a single process.

Caching should follow the data. Versioned JavaScript, CSS, fonts, and public travel images can use long-lived cache headers. Personalised dashboards and mutable form state need private or no-store policies. A cache is useful only when it makes a response faster without showing the wrong user or an unsafe old value.

http
Cache-Control: public, max-age=31536000, immutable
ETag: "route-map-v12"

The ETag lets a client ask whether a representation changed. Content-hashed filenames go one step further: a changed asset receives a new URL, while an unchanged asset remains safely reusable.

4. REST: predictable resources and reliable writes

REST is an architectural style built around resources, stable URLs, standard HTTP methods, stateless requests, and explicit representations. It is not simply 'JSON over HTTP'. For a hypothetical itinerary system, useful resources include itineraries, travellers, interest responses, activities, route stops, photos, and notifications.

http
GET    /api/itineraries/{itineraryId}
GET    /api/itineraries/{itineraryId}/travellers?limit=50
POST   /api/itineraries/{itineraryId}/interest-responses
PATCH  /api/interest-responses/{responseId}
DELETE /api/gallery-photos/{photoId}

The URL names a resource and the method names the action. That convention is easier to document and secure than action-heavy endpoints. Lists should be paginated from the start; returning every traveller or notification may work in a demo and fail when a shared link becomes popular.

Idempotency makes retries safe

A mobile request may reach the server even when the response never reaches the phone. If the user taps Submit again, the backend should recognise the same logical operation instead of creating a duplicate. An idempotency key provides that stable identity.

http
POST /api/itineraries/trip_123/interest-responses HTTP/1.1
Idempotency-Key: traveller_456_activity_2026_08_13
Content-Type: application/json

{
  "travellerId": "traveller_456",
  "interested": true,
  "activityPreference": "sunrise-kayaking"
}

The server stores the key with the first successful result. A retry with the same key returns that result. A conflicting payload with the same key should fail clearly. This is application-level correctness built on a reliable HTTPS connection.

Errors should be structured

http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{
  "error": {
    "code": "ACTIVITY_UNAVAILABLE",
    "message": "This activity is no longer available.",
    "field": "activityPreference"
  }
}

A stable error code helps the client choose the correct interface message and lets observability tools group failures. Returning 200 for every outcome or exposing a vague 500 makes both clients and operators guess.

5. Polling, SSE, and WebSockets

'Real-time' describes a user expectation, not one protocol. Choose the least complex option that meets the freshness and direction requirements.

Polling

Polling asks the server for current state at a fixed interval. It is easy to deploy, observe, and recover. A coordinator dashboard that only needs an updated count every 30 seconds may not need a persistent connection.

http
GET /api/itineraries/trip_123/interest-summary

Server-Sent Events

SSE keeps an HTTP response open so the server can push text events to the browser. It fits one-way streams such as status updates, progress, alerts, and live counts. The browser's EventSource API also provides reconnection behaviour.

text
event: traveller-count
data: {"interested": 218, "notInterested": 17}

event: route-alert
data: {"stopId": "stop_7", "status": "delayed"}

WebSockets

WebSockets keep a full-duplex connection open, so client and server may send messages whenever needed. This suits travel-group chat, collaborative itinerary editing, presence, and other genuinely two-way features.

http
GET /chat HTTP/1.1
Host: journeys.example
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: random-client-key
Sec-WebSocket-Version: 13

A production WebSocket design also needs authentication, room authorisation, heartbeat timeouts, reconnect logic, message persistence, and cross-server fan-out. If one traveller connects to server A and another to server C, a broker such as Redis, NATS, or Kafka can distribute room events between those servers.

6. GraphQL and gRPC solve different problems

GraphQL for client-selected response shapes

A coordinator dashboard may need an itinerary title, traveller count, activity breakdown, route details, and recent messages together. GraphQL lets the client request exactly those fields through one typed schema.

graphql
query JourneyDashboard($itineraryId: ID!) {
  itinerary(id: $itineraryId) {
    title
    travelGroup { names }
    interestSummary { interested notInterested pending }
    activitySummary { activityId interestedCount capacity }
    nextStop { name mapUrl }
  }
}
Reader pulse

How is it so far?

No reaction selected.

Reader pulse

Vote with other readers

The field name is a valid GraphQL identifier; spaces are not. Flexibility shifts work to the backend: teams must control query cost, prevent resolver fan-out, enforce authorisation per field, and design cache behaviour deliberately.

gRPC for controlled internal services

Inside a backend, the interest-response service might call a notification service or analytics pipeline. gRPC uses Protocol Buffers to define strongly typed methods and messages, then generates clients and servers from that contract.

protobuf
syntax = "proto3";

service InterestResponseService {
  rpc RecordInterest (RecordInterestRequest) returns (RecordInterestResponse);
}

message RecordInterestRequest {
  string itinerary_id = 1;
  string traveller_id = 2;
  string activity_preference = 3;
}

message RecordInterestResponse {
  string response_id = 1;
  bool notification_queued = 2;
}

gRPC is often attractive for services controlled by one organisation because it is typed and efficient. Public browsers and third parties frequently benefit from a simpler REST or GraphQL edge, so internal transport choices do not have to become the public API.

7. A practical decision matrix

  • Public itinerary and guide pages: HTTPS with CDN and browser caching.

  • Login or important form submission: HTTPS over a reliable transport, with validation and idempotency where retries can duplicate state.

  • Periodic coordinator totals: polling when a relaxed freshness window is acceptable.

  • One-way live count or route alert: SSE when the browser mainly listens.

  • Two-way chat or collaboration: WebSockets with authentication, persistence, heartbeats, and a fan-out layer.

  • Complex dashboard reads: REST for stable resource shapes or GraphQL when clients need controlled field selection.

  • Internal typed calls: gRPC when both services and their deployment lifecycle are controlled.

  • High-frequency location telemetry: a freshness-first transport or ingestion pipeline, then SSE or WebSockets for browser delivery.

Most features should stay on familiar HTTPS and REST until a different conversation pattern creates a measurable reason to add something else. Operational simplicity is a feature.

8. End-to-end: opening a shared itinerary

  1. DNS resolves the blog host to an address. Cached answers can reduce lookup time.

  2. The browser establishes a transport connection and negotiates TLS so HTTP traffic is encrypted and the server identity is checked.

  3. The browser requests the article or itinerary page. A CDN may serve versioned images, CSS, JavaScript, and other public assets near the traveller.

  4. The user records interest in an activity through a validated REST POST. An idempotency key protects against a duplicate retry.

  5. The coordinator dashboard receives an updated total through polling or SSE, depending on the required freshness.

  6. If travellers join a group chat, a WebSocket provides the two-way channel while a broker carries messages between server instances.

  7. Internal notification and analytics services receive typed calls or asynchronous events without delaying the primary user response.

One tap can involve DNS, TCP or QUIC, TLS, HTTP, cache validators, REST, a live-update channel, and internal service contracts. The architecture is understandable when each protocol is tied to one communication need.

9. Common protocol mistakes

  • Using WebSockets for every feature that looks live, even when polling or SSE is simpler.

  • Calling any JSON endpoint REST without a consistent resource and method model.

  • Ignoring idempotency even though mobile networks can lose responses after a successful write.

  • Choosing UDP only because it sounds faster, without confirming that the product tolerates loss and reordering.

  • Caching personalised or mutable state with the same policy as public static assets.

  • Treating authentication as authorisation; knowing who sent a request does not prove they may perform the action.

  • Scaling WebSockets without a plan for load balancers, timeouts, reconnects, and cross-server fan-out.

  • Assuming GraphQL removes backend complexity instead of moving it into schema, resolver, cost, and cache design.

  • Exposing an internal gRPC contract directly when public clients need a stable HTTP-friendly interface.

10. An interview-ready answer framework

  1. Name the product behaviour: read, write, stream, two-way conversation, telemetry, or service call.

  2. Name the direction: client to server, server to client, both directions, or service to service.

  3. State the correctness requirement: may data be lost, delayed, reordered, retried, or cached?

  4. Choose the simplest protocol that meets those constraints and explain the rejected alternative.

  5. Add production details: timeouts, idempotency, reconnection, pagination, cache policy, load balancing, or schema evolution.

A concise answer might be: 'For an activity-interest submission, I would use HTTPS with a REST POST, structured validation errors, and an idempotency key. I would not use WebSockets because this is a reliable write, not an ongoing two-way conversation.' The reasoning matters more than the number of protocol names.

11. DNS, connections, and what happens before HTTP

A browser cannot request a page from a hostname until it knows where that hostname points. DNS translates a name into an address. The operating system and recursive resolver may already cache the answer. Otherwise the resolver follows the DNS hierarchy until an authoritative server supplies it. Time-to-live values control how long answers may be reused.

DNS commonly uses UDP for small queries because a lookup is a short request-response exchange. It can use TCP when an answer is too large or for operations such as zone transfer. Modern encrypted DNS transports may carry queries over HTTPS or TLS. The important design lesson is that a familiar function can use different transports when its payload and security requirements change.

After name resolution, the client opens a connection. HTTP/1.1 and HTTP/2 normally use TCP. HTTP/3 uses QUIC over UDP, but QUIC adds reliable streams, congestion control, encryption, and recovery above UDP. Saying 'HTTP always uses TCP' is therefore incomplete; the accurate answer depends on the HTTP version.

Connection reuse matters too. Opening a new transport and TLS session for every asset adds latency. Keep-alive, HTTP/2 multiplexing, and HTTP/3 streams let several requests share connection work. A CDN reduces both distance and repeated origin work by serving public assets from an edge location.

12. HTTP details that affect real systems

Methods and status codes are part of the contract

  • GET reads a representation and should not create business state.

  • POST creates a resource or starts processing when the operation is not naturally idempotent.

  • PUT replaces a complete resource at a known URL and is normally idempotent.

  • PATCH changes selected fields and must define how partial validation works.

  • DELETE removes or deactivates a resource according to the product's retention rules.

  • 2xx means the server completed the request; 3xx directs the client elsewhere; 4xx describes a client-side condition; 5xx signals that the server could not complete a valid request.

Status codes should be specific enough to drive client behaviour. A 401 means authentication is needed. A 403 means the identified actor is not allowed. A 404 says the resource is unavailable at that URL. A 409 fits a state conflict, while a 422 can express a well-formed request whose field values fail business validation. A 429 should include a clear retry policy.

Cookies, tokens, and permissions

HTTP is stateless, so authentication state is carried explicitly. A secure session cookie should use HTTPS-only and script-access restrictions where appropriate, and cross-site behaviour should be intentional. Bearer tokens must be short-lived, scoped, and protected because possession is authority. Neither mechanism replaces server-side authorisation.

Authentication answers 'who is this?' Authorisation answers 'may this identity read or change this resource?' A traveller who can read a shared itinerary should not automatically gain access to the coordinator dashboard. Every sensitive request needs an ownership or role check at the server, even when the interface already hides the control.

Timeouts, retries, and backoff

Every network call needs a timeout. Without one, a stalled dependency can hold connections and workers indefinitely. Retrying can help transient failures, but a retry should use bounded exponential backoff with jitter and should only repeat operations that are idempotent or protected by an idempotency key. Otherwise a recovery mechanism can multiply load or duplicate state.

13. Long polling and live-delivery failure modes

Long polling sits between ordinary polling and a continuous stream. The client sends a request and the server holds it until data changes or a timeout expires. After receiving a response, the client immediately opens another request. It reduces empty responses but still creates repeated HTTP request lifecycles.

http
GET /api/itineraries/trip_123/updates?after=event_892 HTTP/1.1
Prefer: wait=30

Polling, long polling, SSE, and WebSockets all need a recovery model. A reconnecting client should identify the last event it processed so the server can replay a bounded gap or instruct the client to fetch current state. Events should carry stable identifiers, and handlers should tolerate a duplicate delivery. 'Real-time' without reconnection and deduplication is only a happy-path demo.

Backpressure matters when producers create events faster than a client can consume them. The system may coalesce location points, cap a queue, disconnect a slow consumer, or persist events for later reads. The correct choice depends on whether every event is valuable or whether current state is enough.

14. REST constraints, evolution, and alternatives

The core REST constraints

  • Client-server separation keeps interface concerns apart from storage and business logic.

  • Stateless requests contain enough context for the server to evaluate them independently.

  • Cacheable responses declare when a representation may be reused.

  • A uniform interface gives resources stable identities and uses standard methods consistently.

  • Layered systems allow gateways, caches, and proxies without requiring the client to know every hop.

HATEOAS is the REST idea that a representation can include links describing valid next actions. Many practical JSON APIs use only part of REST and rely on external documentation instead. It is still useful to understand the constraint because it explains how hypermedia can reduce hard-coded client knowledge.

json
{
  "id": "trip_123",
  "title": "Kerala Coast and Backwaters",
  "links": {
    "self": "/api/itineraries/trip_123",
    "activities": "/api/itineraries/trip_123/activities",
    "interestResponses": "/api/itineraries/trip_123/interest-responses"
  }
}

API versioning

An API evolves while old clients may remain in use. Compatible additions are usually safer than changing the meaning or type of an existing field. Breaking changes can use a path version, media type, or negotiated contract. Whichever strategy is chosen, deprecation dates, telemetry, and migration guidance are more important than the shape of the version label.

SOAP still has a place

SOAP uses XML envelopes and a formal service contract, commonly described through WSDL. Its standards support features such as message-level security and transactional workflows. It is heavier than a typical JSON API, but some enterprise integrations value that strict contract and mature tooling. 'Modern' is not a substitute for matching the organisation's requirements.

15. Security at protocol boundaries

  • Validate the Host and origin assumptions at gateways; do not trust arbitrary forwarded headers.

  • Use TLS everywhere sensitive data crosses a network, including internal service links where threat models require it.

  • Set request-size and parsing limits before expensive business logic.

  • Rate-limit authentication, writes, and costly queries with actor-aware and network-aware controls.

  • Authorise every resource, field, stream subscription, and socket room on the server.

  • Avoid putting secrets or personal data in URLs because URLs appear in logs, history, and referrer metadata.

  • Define CORS narrowly; it is a browser read policy, not an authentication mechanism.

  • Log stable request and event identifiers, but redact credentials and sensitive payloads.

Protocol choice changes the attack surface. A public GraphQL endpoint needs depth, complexity, and field-level permission controls. A WebSocket needs authorisation at connection time and again when joining a room. An internal gRPC method needs service identity and method-level policy. Security must follow the communication contract rather than being added only at the login page.

16. Observability and operating the conversation

A protocol decision is incomplete until the team can observe it. HTTP metrics should separate route, method, status class, latency, and dependency time. A live channel needs connection counts, reconnect rates, message lag, queue depth, dropped-event counts, and broker health. gRPC needs method status and deadline metrics. DNS and TLS failures should be distinguishable from application errors.

Carry a correlation identifier across the public request and internal calls. Distributed tracing can show whether latency comes from edge work, database access, a notification service, or a retry. Structured logs should record the operation and result without storing sensitive content.

Service-level objectives convert protocol behaviour into an operating promise: for example, a percentage of itinerary reads completing below a latency threshold or a bounded delay for route alerts. Alerts should focus on user-visible failure, not only CPU or raw request volume.

17. Protocol cheat sheet

  • TCP: ordered, reliable byte stream; strong default for important web and service traffic.

  • UDP: independent datagrams; useful when low overhead and fresh state outweigh guaranteed delivery.

  • QUIC: reliable encrypted multiplexed streams over UDP; transport for HTTP/3.

  • TLS: encryption, integrity, and peer identity for data in transit.

  • HTTP: request-response semantics, methods, headers, status codes, bodies, and cache rules.

  • REST: resource-oriented API style using a uniform HTTP interface and stateless requests.

  • Polling: repeated reads; simple and effective for relaxed freshness.

  • Long polling: a held request followed by immediate renewal; fewer empty responses than fixed polling.

  • SSE: one-way text event stream from server to browser with browser reconnection support.

  • WebSocket: persistent full-duplex channel for two-way, low-latency interaction.

  • GraphQL: typed query language for controlled client-selected response shapes.

  • gRPC: generated, strongly typed remote methods, commonly used between controlled services.

  • Message queue or log: asynchronous communication that decouples producers from consumers.

18. More interview scenarios

Public travel guide

Use HTTPS, cacheable HTML or server rendering, an image CDN, content-hashed static assets, and a clear invalidation path. Explain how stale content is bounded and how the origin behaves when the database is temporarily unavailable.

Live route status

Use polling when a 30-second delay is acceptable, SSE for immediate server-to-browser alerts, or WebSockets only if the user also sends frequent live control messages. Include event identifiers, reconnect behaviour, and a current-state endpoint for recovery.

Collaborative itinerary editing

Use a WebSocket for changes and presence, but persist authoritative document versions. Define ordering, conflict resolution, authorisation, reconnect replay, and how multiple server instances exchange operations. The socket is only the transport; collaboration semantics remain an application problem.

Booking or payment

Use HTTPS with an idempotent application contract, server-side validation, a database transaction, a stable state machine, and provider webhooks verified independently. Never treat a client redirect alone as payment proof. This hypothetical example does not mean booking is currently available on Travel With Snigdha.

Internal notification delivery

If the user-facing write should not wait for notification delivery, publish a durable event in the same transaction boundary or through an outbox. Consumers retry with deduplication and move poison messages to a reviewed failure path. Use gRPC for a synchronous typed call only when the dependency belongs on the critical path.

19. Further reading

For deeper study, use primary specifications and operational documentation: IETF RFCs for TCP, UDP, TLS, HTTP, QUIC, and the WebSocket wire protocol; the WHATWG HTML documentation for EventSource and Server-Sent Events plus the browser WebSocket API; the GraphQL specification; and the gRPC and Protocol Buffers documentation. Specifications explain the contract, while production postmortems explain where real implementations fail.

Final takeaway

Protocols are communication contracts. TCP and UDP define transport tradeoffs. TLS protects traffic. HTTP structures web requests and responses. REST gives public APIs a predictable resource model. Polling, SSE, and WebSockets serve different live-update directions. GraphQL shapes complex client reads, while gRPC strengthens controlled service contracts.

When a system-design question feels crowded, return to five questions: who is talking, in which direction, how reliable must it be, how fresh must it be, and what operational complexity can the team support? Answer those, and the protocol choice usually becomes much less mysterious.

Reader checkpoint

Lock in the takeaway

Frequently asked questions

What is a protocol in system design?

A protocol is a shared communication contract. It defines how participants connect, format data, signal success or failure, protect traffic, and recover when communication is interrupted.

When should I choose TCP instead of UDP?

Choose TCP when complete, ordered delivery matters, such as page loads, authentication, form submissions, API writes, and database connections. Choose a UDP-based approach only when the product can tolerate loss or prefers a fresh update over replaying an old one.

Does HTTP use TCP or UDP?

HTTP/1.1 and HTTP/2 normally run over TCP. HTTP/3 runs over QUIC, which uses UDP while providing reliability, streams, encryption, and congestion control above the transport layer.

What is the difference between HTTP and HTTPS?

HTTPS is HTTP protected by TLS. TLS encrypts traffic, checks integrity, and helps the client verify the server's identity, which is why public and authenticated web traffic should use HTTPS.

Is every JSON API a REST API?

No. REST is an architectural style that models resources with stable URLs, uses HTTP methods consistently, keeps requests stateless, and represents success and errors clearly. Returning JSON alone does not make an endpoint RESTful.

Why are idempotency keys useful?

A request can succeed on the server even when its response is lost. An idempotency key lets a retry return the original result instead of creating duplicate state for the same logical operation.

When is polling better than WebSockets?

Polling is often better when updates can arrive every few seconds or minutes and operational simplicity matters. WebSockets are justified when both sides need a persistent, low-latency channel.

When should I use Server-Sent Events?

Use SSE when the server needs to push a text stream and the browser mostly listens, such as live counts, progress, alerts, or status updates. It is simpler than a full two-way socket for one-directional communication.

When should I use WebSockets?

Use WebSockets for genuine two-way, low-latency features such as chat, presence, collaborative editing, or interactive controls. Plan for authentication, heartbeats, reconnects, persistence, load balancers, and cross-server fan-out.

When is GraphQL useful?

GraphQL is useful when clients need different controlled combinations of related fields. It reduces over-fetching but requires careful query-cost limits, field authorisation, resolver performance, and cache design.

When is gRPC useful?

gRPC is useful for typed service-to-service calls when one organisation controls both sides. Protocol Buffer contracts generate clients and servers, while public clients may still use a REST or GraphQL edge.

How should I choose a protocol in an interview?

Describe the product behaviour, communication direction, reliability and freshness needs, cacheability, retry behaviour, and operational constraints. Then choose the simplest protocol that satisfies those requirements and explain the tradeoff.

Reader discussion

What readers think

0 comments
0/1200